@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifies the agent loop's exclusive-tool dispatch: when a tool the loop is
|
|
3
|
+
* told is exclusive (e.g. the advisor) appears in a multi-call turn, only that
|
|
4
|
+
* tool runs and the siblings are deferred un-run with a benign result — so the
|
|
5
|
+
* model incorporates the exclusive tool's output before acting on anything
|
|
6
|
+
* else. Drives the REAL loop, mocking only the provider boundary.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
import { createMockProvider } from "../__tests__/helpers/mock-provider.js";
|
|
11
|
+
import type { ContentBlock, ProviderResponse } from "../providers/types.js";
|
|
12
|
+
import { AgentLoop } from "./loop.js";
|
|
13
|
+
|
|
14
|
+
const endTurn = (text: string): ProviderResponse => ({
|
|
15
|
+
content: [{ type: "text", text }],
|
|
16
|
+
model: "mock-model",
|
|
17
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
18
|
+
stopReason: "end_turn",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const toolUseTurn = (
|
|
22
|
+
blocks: Array<{ id: string; name: string }>,
|
|
23
|
+
): ProviderResponse => ({
|
|
24
|
+
content: [
|
|
25
|
+
{ type: "text", text: "working" },
|
|
26
|
+
...blocks.map((b) => ({
|
|
27
|
+
type: "tool_use" as const,
|
|
28
|
+
id: b.id,
|
|
29
|
+
name: b.name,
|
|
30
|
+
input: {},
|
|
31
|
+
})),
|
|
32
|
+
],
|
|
33
|
+
model: "mock-model",
|
|
34
|
+
usage: { inputTokens: 1, outputTokens: 1 },
|
|
35
|
+
stopReason: "tool_use",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
function toolResults(history: { content: ContentBlock[] }[]) {
|
|
39
|
+
return history
|
|
40
|
+
.flatMap((m) => m.content)
|
|
41
|
+
.filter(
|
|
42
|
+
(b): b is Extract<ContentBlock, { type: "tool_result" }> =>
|
|
43
|
+
b.type === "tool_result",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const baseRun = {
|
|
48
|
+
requestId: "req-excl",
|
|
49
|
+
onEvent: () => {},
|
|
50
|
+
callSite: "mainAgent" as const,
|
|
51
|
+
trust: { sourceChannel: "vellum" as const, trustClass: "unknown" as const },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
describe("AgentLoop — exclusive tool deferral", () => {
|
|
55
|
+
test("runs the exclusive tool alone and defers sibling calls un-run", async () => {
|
|
56
|
+
const { provider } = createMockProvider([
|
|
57
|
+
toolUseTurn([
|
|
58
|
+
{ id: "call-advisor", name: "advisor" },
|
|
59
|
+
{ id: "call-edit", name: "write_file" },
|
|
60
|
+
]),
|
|
61
|
+
endTurn("done"),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const executed: string[] = [];
|
|
65
|
+
const loop = new AgentLoop({
|
|
66
|
+
provider,
|
|
67
|
+
systemPrompt: "sys",
|
|
68
|
+
conversationId: "excl-1",
|
|
69
|
+
tools: [
|
|
70
|
+
{ name: "advisor", description: "", input_schema: { type: "object" } },
|
|
71
|
+
{
|
|
72
|
+
name: "write_file",
|
|
73
|
+
description: "",
|
|
74
|
+
input_schema: { type: "object" },
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
toolExecutor: async (name) => {
|
|
78
|
+
executed.push(name);
|
|
79
|
+
return { content: `ran ${name}`, isError: false };
|
|
80
|
+
},
|
|
81
|
+
isExclusiveTool: (name) => name === "advisor",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const { history } = await loop.run({
|
|
85
|
+
...baseRun,
|
|
86
|
+
messages: [{ role: "user", content: [{ type: "text", text: "do it" }] }],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Only the exclusive tool actually executed.
|
|
90
|
+
expect(executed).toEqual(["advisor"]);
|
|
91
|
+
|
|
92
|
+
const results = toolResults(history);
|
|
93
|
+
const advisorResult = results.find(
|
|
94
|
+
(b) => b.tool_use_id === "call-advisor",
|
|
95
|
+
)!;
|
|
96
|
+
const editResult = results.find((b) => b.tool_use_id === "call-edit")!;
|
|
97
|
+
|
|
98
|
+
// The advisor ran; the sibling came back un-run (not an error) so the model
|
|
99
|
+
// can re-issue it after reading the guidance.
|
|
100
|
+
expect(advisorResult.content).toBe("ran advisor");
|
|
101
|
+
expect(editResult.content).toContain("not run");
|
|
102
|
+
expect(editResult.content).toContain("advisor");
|
|
103
|
+
expect(editResult.is_error).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("runs sibling tools normally when no exclusive tool is present", async () => {
|
|
107
|
+
const { provider } = createMockProvider([
|
|
108
|
+
toolUseTurn([
|
|
109
|
+
{ id: "call-read", name: "read_file" },
|
|
110
|
+
{ id: "call-write", name: "write_file" },
|
|
111
|
+
]),
|
|
112
|
+
endTurn("done"),
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const executed: string[] = [];
|
|
116
|
+
const loop = new AgentLoop({
|
|
117
|
+
provider,
|
|
118
|
+
systemPrompt: "sys",
|
|
119
|
+
conversationId: "excl-2",
|
|
120
|
+
tools: [
|
|
121
|
+
{
|
|
122
|
+
name: "read_file",
|
|
123
|
+
description: "",
|
|
124
|
+
input_schema: { type: "object" },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "write_file",
|
|
128
|
+
description: "",
|
|
129
|
+
input_schema: { type: "object" },
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
toolExecutor: async (name) => {
|
|
133
|
+
executed.push(name);
|
|
134
|
+
return { content: `ran ${name}`, isError: false };
|
|
135
|
+
},
|
|
136
|
+
isExclusiveTool: (name) => name === "advisor",
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const { history } = await loop.run({
|
|
140
|
+
...baseRun,
|
|
141
|
+
messages: [{ role: "user", content: [{ type: "text", text: "do it" }] }],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Both non-exclusive tools ran; nothing was deferred.
|
|
145
|
+
expect(executed.sort()).toEqual(["read_file", "write_file"]);
|
|
146
|
+
for (const result of toolResults(history)) {
|
|
147
|
+
expect(result.content).not.toContain("not run");
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
package/src/agent/loop.ts
CHANGED
|
@@ -625,6 +625,20 @@ export type LoopToolExecutor = (
|
|
|
625
625
|
activityMetadata?: ToolActivityMetadata;
|
|
626
626
|
}>;
|
|
627
627
|
|
|
628
|
+
/**
|
|
629
|
+
* The benign result returned for a sibling tool call that was deferred because
|
|
630
|
+
* an exclusive tool ran in the same turn. Phrased so the model treats it as a
|
|
631
|
+
* "not run yet" signal — read the exclusive tool's output, then re-issue this
|
|
632
|
+
* call if it is still the right next step.
|
|
633
|
+
*/
|
|
634
|
+
function deferredForExclusiveMessage(exclusiveToolName: string): string {
|
|
635
|
+
return (
|
|
636
|
+
`(not run: \`${exclusiveToolName}\` was called this turn and runs first, on its own, ` +
|
|
637
|
+
`so the rest of your tool calls were held back. Read its output, then call this tool ` +
|
|
638
|
+
`again if it is still the right next step.)`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
628
642
|
export interface AgentLoopConstructorOptions {
|
|
629
643
|
/** LLM provider the loop issues every call through. */
|
|
630
644
|
provider: Provider;
|
|
@@ -634,6 +648,14 @@ export interface AgentLoopConstructorOptions {
|
|
|
634
648
|
tools?: ToolDefinition[];
|
|
635
649
|
toolExecutor?: LoopToolExecutor;
|
|
636
650
|
resolveTools?: (history: Message[]) => ToolDefinition[];
|
|
651
|
+
/**
|
|
652
|
+
* Decide whether a tool runs exclusively in its turn (see
|
|
653
|
+
* {@link ToolDefinition.exclusive}). When it returns true for a tool present
|
|
654
|
+
* in a multi-call turn, the loop runs only that tool and defers the siblings
|
|
655
|
+
* un-run. Injected by the conversation wiring, which can read the tool
|
|
656
|
+
* registry; lightweight loops that omit it never defer.
|
|
657
|
+
*/
|
|
658
|
+
isExclusiveTool?: (toolName: string) => boolean;
|
|
637
659
|
/**
|
|
638
660
|
* Conversation this loop drives. Scopes the loop-held compaction circuit
|
|
639
661
|
* breaker and is the source of truth the loop's pipeline contexts and
|
|
@@ -659,6 +681,7 @@ export class AgentLoop {
|
|
|
659
681
|
private tools: ToolDefinition[];
|
|
660
682
|
private resolveTools: ((history: Message[]) => ToolDefinition[]) | null;
|
|
661
683
|
private toolExecutor: LoopToolExecutor | null;
|
|
684
|
+
private isExclusiveTool: ((toolName: string) => boolean) | null;
|
|
662
685
|
|
|
663
686
|
/**
|
|
664
687
|
* Conversation this loop drives. Source of truth for the `conversationId`
|
|
@@ -688,6 +711,7 @@ export class AgentLoop {
|
|
|
688
711
|
tools,
|
|
689
712
|
toolExecutor,
|
|
690
713
|
resolveTools,
|
|
714
|
+
isExclusiveTool,
|
|
691
715
|
conversationId,
|
|
692
716
|
resolveConversationDir,
|
|
693
717
|
} = options;
|
|
@@ -697,6 +721,7 @@ export class AgentLoop {
|
|
|
697
721
|
this.tools = tools ?? [];
|
|
698
722
|
this.resolveTools = resolveTools ?? null;
|
|
699
723
|
this.toolExecutor = toolExecutor ?? null;
|
|
724
|
+
this.isExclusiveTool = isExclusiveTool ?? null;
|
|
700
725
|
this.conversationId = conversationId;
|
|
701
726
|
this.resolveConversationDir = resolveConversationDir ?? null;
|
|
702
727
|
this.compactionCircuit = new CompactionCircuit(this.conversationId);
|
|
@@ -1883,8 +1908,39 @@ export class AgentLoop {
|
|
|
1883
1908
|
"Tool execution start",
|
|
1884
1909
|
);
|
|
1885
1910
|
|
|
1911
|
+
// When an exclusive tool (e.g. the advisor) is among this turn's calls,
|
|
1912
|
+
// it must run alone: the model should incorporate its output before
|
|
1913
|
+
// acting on anything else. Run only the first exclusive call and defer
|
|
1914
|
+
// the siblings with a benign, un-run result so the model re-issues them
|
|
1915
|
+
// next turn if still needed. Every tool_use still gets a matching
|
|
1916
|
+
// tool_result, so history stays well-formed.
|
|
1917
|
+
const exclusiveBlock = this.isExclusiveTool
|
|
1918
|
+
? toolUseBlocks.find((block) => this.isExclusiveTool!(block.name))
|
|
1919
|
+
: undefined;
|
|
1920
|
+
const deferSiblings =
|
|
1921
|
+
exclusiveBlock !== undefined && toolUseBlocks.length > 1;
|
|
1922
|
+
if (deferSiblings) {
|
|
1923
|
+
rlog.info(
|
|
1924
|
+
{
|
|
1925
|
+
turn: toolUseTurns,
|
|
1926
|
+
exclusiveTool: exclusiveBlock!.name,
|
|
1927
|
+
deferred: toolUseBlocks
|
|
1928
|
+
.filter((block) => block !== exclusiveBlock)
|
|
1929
|
+
.map((block) => block.name),
|
|
1930
|
+
},
|
|
1931
|
+
"Exclusive tool present — running it alone and deferring sibling tool calls this turn",
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1886
1935
|
const toolExecutionPromise = Promise.all(
|
|
1887
1936
|
toolUseBlocks.map(async (toolUse) => {
|
|
1937
|
+
if (deferSiblings && toolUse !== exclusiveBlock) {
|
|
1938
|
+
const result: Awaited<ReturnType<LoopToolExecutor>> = {
|
|
1939
|
+
content: deferredForExclusiveMessage(exclusiveBlock!.name),
|
|
1940
|
+
isError: false,
|
|
1941
|
+
};
|
|
1942
|
+
return { toolUse, result };
|
|
1943
|
+
}
|
|
1888
1944
|
const result = await this.toolExecutor!(
|
|
1889
1945
|
toolUse.name,
|
|
1890
1946
|
toolUse.input,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maximum number of events the daemon's per-process SSE replay ring
|
|
3
|
+
* retains for `Last-Event-ID` resume. This is the ring's count bound; the
|
|
4
|
+
* ring is also bounded by total bytes and entry age (whichever limit is
|
|
5
|
+
* hit first wins), so the live ring can hold *fewer* than this many
|
|
6
|
+
* events, never more. The daemon-side definition and eviction live in
|
|
7
|
+
* `assistant/src/runtime/assistant-stream-state.ts`.
|
|
8
|
+
*
|
|
9
|
+
* Exposed on the API surface so the web client's SSE consumer can size
|
|
10
|
+
* its seq-gap tolerance against the same number the daemon buffers
|
|
11
|
+
* against, instead of hard-coding a duplicate.
|
|
12
|
+
*
|
|
13
|
+
* A live seq gap smaller than this is benign: the global per-assistant
|
|
14
|
+
* `seq` counter is stamped before fanout, but the hub deliberately
|
|
15
|
+
* withholds some events from a given subscriber — self-echo-suppressed
|
|
16
|
+
* `sync_changed` (a client's own mutation echo) and capability-targeted
|
|
17
|
+
* host-proxy events — so a subscriber legitimately sees its cursor skip a
|
|
18
|
+
* few seqs it was never going to receive. Such a hole is not data loss
|
|
19
|
+
* and must not trigger a destructive authoritative snapshot heal. Only a
|
|
20
|
+
* gap that meets or exceeds this count proves the live suffix fell
|
|
21
|
+
* outside the ring entirely and is genuinely non-contiguous.
|
|
22
|
+
*/
|
|
23
|
+
export const SSE_REPLAY_RING_COUNT_LIMIT = 200;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Maximum age (in milliseconds) an event may reach in the daemon's SSE
|
|
27
|
+
* replay ring before it is evicted, regardless of count or byte usage.
|
|
28
|
+
* The daemon-side definition and eviction live in
|
|
29
|
+
* `assistant/src/runtime/assistant-stream-state.ts`.
|
|
30
|
+
*
|
|
31
|
+
* Exposed alongside the count bound so the web client can reason about
|
|
32
|
+
* the age dimension of the ring too. A small live seq gap is only safe to
|
|
33
|
+
* treat as benign when the client has been continuously receiving events
|
|
34
|
+
* — live delivery is concurrent with stamping, so a connected subscriber
|
|
35
|
+
* never relies on the ring. Once the connection has been quiet for longer
|
|
36
|
+
* than this window (a disconnect/resume), events the client missed may
|
|
37
|
+
* have aged out of the ring and become unrecoverable by replay, so even a
|
|
38
|
+
* small seq gap must trigger an authoritative reconcile rather than be
|
|
39
|
+
* waved through.
|
|
40
|
+
*/
|
|
41
|
+
export const SSE_REPLAY_RING_AGE_LIMIT_MS = 30_000;
|
package/src/api/index.ts
CHANGED
|
@@ -61,6 +61,10 @@ export {
|
|
|
61
61
|
CALL_SITE_COMPACTION_AGENT,
|
|
62
62
|
CALL_SITE_SYNTHETIC_AGENT_ERROR_MESSAGE,
|
|
63
63
|
} from "./constants/call-sites.js";
|
|
64
|
+
export {
|
|
65
|
+
SSE_REPLAY_RING_AGE_LIMIT_MS,
|
|
66
|
+
SSE_REPLAY_RING_COUNT_LIMIT,
|
|
67
|
+
} from "./constants/sse-replay.js";
|
|
64
68
|
export { DEFAULT_TOOL_EXECUTION_TIMEOUT_SEC } from "./constants/tool-execution.js";
|
|
65
69
|
export {
|
|
66
70
|
type AssistantActivityAnchor,
|
|
@@ -430,6 +434,8 @@ export {
|
|
|
430
434
|
LlmContextResponseSchema,
|
|
431
435
|
} from "./responses/llm-context-response.js";
|
|
432
436
|
export {
|
|
437
|
+
type LLMCallError,
|
|
438
|
+
LLMCallErrorSchema,
|
|
433
439
|
type LLMCallSummary,
|
|
434
440
|
LLMCallSummarySchema,
|
|
435
441
|
type LLMContextSection,
|
|
@@ -67,6 +67,30 @@ export const LLMContextSectionSchema = z.object({
|
|
|
67
67
|
|
|
68
68
|
export type LLMContextSection = z.infer<typeof LLMContextSectionSchema>;
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Structured provider/transport error recorded when an LLM call was
|
|
72
|
+
* rejected before producing a response. Mirrors the on-disk
|
|
73
|
+
* `responsePayload.error` shape written by
|
|
74
|
+
* `buildProviderErrorResponsePayload` — the inspector branches on the
|
|
75
|
+
* presence of this field to render a failed call distinctly (failure
|
|
76
|
+
* banner in the Response tab, $0.00 cost in the rail, etc.) instead of
|
|
77
|
+
* the generic "section rendering unavailable" fallback.
|
|
78
|
+
*
|
|
79
|
+
* Every field is optional because the serializer degrades a plain
|
|
80
|
+
* `Error` down to just `{ name, message }`; only the wrapper object is
|
|
81
|
+
* guaranteed.
|
|
82
|
+
*/
|
|
83
|
+
export const LLMCallErrorSchema = z.object({
|
|
84
|
+
name: z.string().nullish(),
|
|
85
|
+
message: z.string().nullish(),
|
|
86
|
+
code: z.string().nullish(),
|
|
87
|
+
provider: z.string().nullish(),
|
|
88
|
+
statusCode: z.number().nullish(),
|
|
89
|
+
retryAfterMs: z.number().nullish(),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export type LLMCallError = z.infer<typeof LLMCallErrorSchema>;
|
|
93
|
+
|
|
70
94
|
/**
|
|
71
95
|
* One LLM request log row.
|
|
72
96
|
*
|
|
@@ -88,6 +112,7 @@ export const LLMRequestLogEntrySchema = z.object({
|
|
|
88
112
|
responseSections: z.array(LLMContextSectionSchema).nullish(),
|
|
89
113
|
agentLoopExitReason: z.string().nullish(),
|
|
90
114
|
callSite: z.string().nullish(),
|
|
115
|
+
error: LLMCallErrorSchema.nullish(),
|
|
91
116
|
});
|
|
92
117
|
|
|
93
118
|
export type LLMRequestLogEntry = z.infer<typeof LLMRequestLogEntrySchema>;
|
|
@@ -31,6 +31,23 @@ export const SubagentDetailEventSchema = z.object({
|
|
|
31
31
|
toolName: z.string().optional(),
|
|
32
32
|
isError: z.boolean().optional(),
|
|
33
33
|
messageId: z.string().optional(),
|
|
34
|
+
/**
|
|
35
|
+
* Tool-call id — the `tool_use.id` on a tool-call event and the referencing
|
|
36
|
+
* `tool_use_id` on its tool-result event, in the daemon's canonical
|
|
37
|
+
* content-block format. That format is provider-agnostic: every provider
|
|
38
|
+
* (Anthropic, OpenAI, Gemini, …) normalizes its native tool calls into these
|
|
39
|
+
* `tool_use`/`tool_result` blocks (see `providers/types.ts`), so this id is
|
|
40
|
+
* present regardless of which model produced the call. Lets the web client
|
|
41
|
+
* pair a result with its call and key the nested tool-detail view, so tool
|
|
42
|
+
* pills on reloaded/history subagents are clickable (not just live ones).
|
|
43
|
+
*/
|
|
44
|
+
toolUseId: z.string().optional(),
|
|
45
|
+
/**
|
|
46
|
+
* Raw tool input object on tool-call events. (`content` also carries a
|
|
47
|
+
* JSON-stringified copy for back-compat / label derivation.) Surfaced in the
|
|
48
|
+
* tool-detail view's input section.
|
|
49
|
+
*/
|
|
50
|
+
input: z.record(z.string(), z.unknown()).optional(),
|
|
34
51
|
});
|
|
35
52
|
|
|
36
53
|
export type SubagentDetailEvent = z.infer<typeof SubagentDetailEventSchema>;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
17
17
|
|
|
18
|
-
import type { AdmissionPolicy } from "@vellumai/gateway-client";
|
|
18
|
+
import type { AdmissionPolicy, TrustVerdict } from "@vellumai/gateway-client";
|
|
19
19
|
|
|
20
20
|
import type {
|
|
21
21
|
ChannelPolicy,
|
|
@@ -38,10 +38,21 @@ mock.module("../../config/loader.js", () => ({
|
|
|
38
38
|
getConfig: () => ({ calls: { verification: { enabled: false } } }),
|
|
39
39
|
}));
|
|
40
40
|
|
|
41
|
-
// Controllable resolved trust context.
|
|
41
|
+
// Controllable resolved trust context. `resolveActorTrust` is a tracked mock
|
|
42
|
+
// so the verdict-source tests can assert the local fallback fires (or not).
|
|
43
|
+
// The verdict path uses the REAL, pure `actorTrustContextFromVerdict` /
|
|
44
|
+
// `verdictMemberUnresolvable` — no module mock — so this file leaks nothing
|
|
45
|
+
// into sibling test files sharing the bun process.
|
|
42
46
|
let nextTrust: ActorTrustContext;
|
|
47
|
+
const resolveActorTrustMock = mock(() => nextTrust);
|
|
48
|
+
// Override only `resolveActorTrust`; the real `trust-verdict-consumer` imports
|
|
49
|
+
// `toTrustContext` from this module, so the rest must pass through untouched.
|
|
50
|
+
const actorTrustResolverModule = await import(
|
|
51
|
+
"../../runtime/actor-trust-resolver.js"
|
|
52
|
+
);
|
|
43
53
|
mock.module("../../runtime/actor-trust-resolver.js", () => ({
|
|
44
|
-
|
|
54
|
+
...actorTrustResolverModule,
|
|
55
|
+
resolveActorTrust: resolveActorTrustMock,
|
|
45
56
|
}));
|
|
46
57
|
|
|
47
58
|
// Controllable pending verification challenge.
|
|
@@ -151,13 +162,17 @@ function makeTrust(
|
|
|
151
162
|
};
|
|
152
163
|
}
|
|
153
164
|
|
|
154
|
-
function route(
|
|
165
|
+
function route(
|
|
166
|
+
admissionPolicy?: AdmissionPolicy | null,
|
|
167
|
+
verdict?: TrustVerdict | null,
|
|
168
|
+
) {
|
|
155
169
|
return routeSetup({
|
|
156
170
|
callSessionId: "cs_1",
|
|
157
171
|
session: null, // inbound
|
|
158
172
|
from: "+12025550142",
|
|
159
173
|
to: "+12025550199",
|
|
160
174
|
admissionPolicy,
|
|
175
|
+
verdict,
|
|
161
176
|
});
|
|
162
177
|
}
|
|
163
178
|
|
|
@@ -165,6 +180,7 @@ beforeEach(() => {
|
|
|
165
180
|
pendingChallenge = null;
|
|
166
181
|
activeInvites = [];
|
|
167
182
|
boundContact = null;
|
|
183
|
+
resolveActorTrustMock.mockClear();
|
|
168
184
|
});
|
|
169
185
|
|
|
170
186
|
// ---------------------------------------------------------------------------
|
|
@@ -374,3 +390,245 @@ describe("routeSetup — floor bypasses", () => {
|
|
|
374
390
|
expect(outcome.action).toBe("verification");
|
|
375
391
|
});
|
|
376
392
|
});
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Caller-trust source: gateway verdict first, local fallback
|
|
396
|
+
// ---------------------------------------------------------------------------
|
|
397
|
+
|
|
398
|
+
function makeVerdict(overrides: Partial<TrustVerdict> = {}): TrustVerdict {
|
|
399
|
+
return {
|
|
400
|
+
trustClass: "guardian",
|
|
401
|
+
canonicalSenderId: "+12025550142",
|
|
402
|
+
...overrides,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// A verdict carrying a fully-resolvable member ACL (contactId/channelId + valid
|
|
407
|
+
// known status·policy enums). The REAL `resolvedMemberFromVerdict` synthesizes
|
|
408
|
+
// a memberRecord from these, so the verdict path enforces blocked/revoked/deny.
|
|
409
|
+
function makeMemberVerdict(
|
|
410
|
+
trustClass: TrustVerdict["trustClass"],
|
|
411
|
+
channel: { status: string; policy?: string },
|
|
412
|
+
overrides: Partial<TrustVerdict> = {},
|
|
413
|
+
): TrustVerdict {
|
|
414
|
+
return makeVerdict({
|
|
415
|
+
trustClass,
|
|
416
|
+
contactId: "ct_1",
|
|
417
|
+
channelId: "ch_1",
|
|
418
|
+
status: channel.status,
|
|
419
|
+
policy: channel.policy ?? "allow",
|
|
420
|
+
...overrides,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
describe("routeSetup — caller-trust source", () => {
|
|
425
|
+
test("present verdict builds trust from the verdict (no local resolve)", () => {
|
|
426
|
+
const { resolved, outcome } = route(
|
|
427
|
+
null,
|
|
428
|
+
makeMemberVerdict("guardian", { status: "active" }),
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
432
|
+
expect(resolved.actorTrust.trustClass).toBe("guardian");
|
|
433
|
+
expect(outcome.action).toBe("normal_call");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("resolutionFailed verdict falls back to local resolveActorTrust", () => {
|
|
437
|
+
nextTrust = makeTrust("guardian", { status: "active", role: "guardian" });
|
|
438
|
+
const { resolved } = route(null, makeVerdict({ resolutionFailed: true }));
|
|
439
|
+
|
|
440
|
+
expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
|
|
441
|
+
expect(resolved.actorTrust.trustClass).toBe("guardian");
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("null verdict falls back to local resolveActorTrust", () => {
|
|
445
|
+
nextTrust = makeTrust("trusted_contact", { status: "active" });
|
|
446
|
+
const { resolved } = route(null, null);
|
|
447
|
+
|
|
448
|
+
expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
|
|
449
|
+
expect(resolved.actorTrust.trustClass).toBe("trusted_contact");
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("absent verdict falls back to local resolveActorTrust", () => {
|
|
453
|
+
nextTrust = makeTrust("guardian", { status: "active", role: "guardian" });
|
|
454
|
+
route(null);
|
|
455
|
+
|
|
456
|
+
expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("admission floor still applies on the verdict path (guardian_only denies trusted_contact)", () => {
|
|
460
|
+
const { outcome } = route(
|
|
461
|
+
"guardian_only",
|
|
462
|
+
makeMemberVerdict("trusted_contact", { status: "active" }),
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
466
|
+
expect(outcome.action).toBe("deny");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("admission floor still applies on the fallback path (guardian_only denies trusted_contact)", () => {
|
|
470
|
+
nextTrust = makeTrust("trusted_contact", { status: "active" });
|
|
471
|
+
const { outcome } = route(
|
|
472
|
+
"guardian_only",
|
|
473
|
+
makeVerdict({ resolutionFailed: true }),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
|
|
477
|
+
expect(outcome.action).toBe("deny");
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// ---------------------------------------------------------------------------
|
|
482
|
+
// Verdict-path ACL: blocked / revoked / deny enforced from the verdict-derived
|
|
483
|
+
// memberRecord (no local fallback). Guards the P1 where a verdict member with
|
|
484
|
+
// no memberRecord bypassed these gates.
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
describe("routeSetup — verdict path enforces member ACL", () => {
|
|
488
|
+
test("blocked member via verdict is denied (not normal_call) under permissive floor", () => {
|
|
489
|
+
const { outcome } = route(
|
|
490
|
+
"strangers",
|
|
491
|
+
makeMemberVerdict("unknown", { status: "blocked" }),
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
495
|
+
expect(outcome.action).toBe("deny");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("revoked member via verdict is denied under permissive floor", () => {
|
|
499
|
+
const { outcome } = route(
|
|
500
|
+
"strangers",
|
|
501
|
+
makeMemberVerdict("unknown", { status: "revoked" }),
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
505
|
+
expect(outcome.action).toBe("deny");
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("policy deny member via verdict is denied (not normal_call)", () => {
|
|
509
|
+
const { outcome } = route(
|
|
510
|
+
null,
|
|
511
|
+
makeMemberVerdict("trusted_contact", {
|
|
512
|
+
status: "active",
|
|
513
|
+
policy: "deny",
|
|
514
|
+
}),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
518
|
+
expect(outcome.action).toBe("deny");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("policy escalate member via verdict is denied (live call can't await approval)", () => {
|
|
522
|
+
const { outcome } = route(
|
|
523
|
+
null,
|
|
524
|
+
makeMemberVerdict("trusted_contact", {
|
|
525
|
+
status: "active",
|
|
526
|
+
policy: "escalate",
|
|
527
|
+
}),
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
531
|
+
expect(outcome.action).toBe("deny");
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("trusted/active member via verdict still admits to normal_call", () => {
|
|
535
|
+
const { outcome } = route(
|
|
536
|
+
null,
|
|
537
|
+
makeMemberVerdict("trusted_contact", {
|
|
538
|
+
status: "active",
|
|
539
|
+
policy: "allow",
|
|
540
|
+
}),
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
544
|
+
expect(outcome.action).toBe("normal_call");
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("guardian via verdict still admits to normal_call", () => {
|
|
548
|
+
const { outcome } = route(
|
|
549
|
+
null,
|
|
550
|
+
makeMemberVerdict("guardian", { status: "active" }),
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
554
|
+
expect(outcome.action).toBe("normal_call");
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
// Unresolvable member verdict → local fallback (never trust an un-ACL-checkable
|
|
560
|
+
// member). A verdict claiming a member (contactId/channelId) whose ACL can't be
|
|
561
|
+
// reassembled (missing/unknown status·policy) must take the local resolveActorTrust
|
|
562
|
+
// path so the member is ACL-checked locally, not trusted by trustClass.
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
describe("routeSetup — unresolvable member verdict falls back to local", () => {
|
|
566
|
+
test("member identity with missing status falls back to local resolveActorTrust", () => {
|
|
567
|
+
nextTrust = makeTrust("trusted_contact", { status: "active" });
|
|
568
|
+
const { resolved } = route(
|
|
569
|
+
null,
|
|
570
|
+
makeVerdict({
|
|
571
|
+
trustClass: "trusted_contact",
|
|
572
|
+
contactId: "ct_1",
|
|
573
|
+
channelId: "ch_1",
|
|
574
|
+
policy: "allow",
|
|
575
|
+
// status absent → unresolvable
|
|
576
|
+
}),
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
|
|
580
|
+
expect(resolved.actorTrust.trustClass).toBe("trusted_contact");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("member identity with unknown status falls back to local resolveActorTrust", () => {
|
|
584
|
+
nextTrust = makeTrust("trusted_contact", { status: "active" });
|
|
585
|
+
route(
|
|
586
|
+
null,
|
|
587
|
+
makeVerdict({
|
|
588
|
+
trustClass: "trusted_contact",
|
|
589
|
+
contactId: "ct_1",
|
|
590
|
+
channelId: "ch_1",
|
|
591
|
+
status: "bogus",
|
|
592
|
+
policy: "allow",
|
|
593
|
+
}),
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("member identity with unknown policy falls back to local resolveActorTrust", () => {
|
|
600
|
+
nextTrust = makeTrust("trusted_contact", { status: "active" });
|
|
601
|
+
route(
|
|
602
|
+
null,
|
|
603
|
+
makeVerdict({
|
|
604
|
+
trustClass: "trusted_contact",
|
|
605
|
+
contactId: "ct_1",
|
|
606
|
+
channelId: "ch_1",
|
|
607
|
+
status: "active",
|
|
608
|
+
policy: "bogus",
|
|
609
|
+
}),
|
|
610
|
+
);
|
|
611
|
+
|
|
612
|
+
expect(resolveActorTrustMock).toHaveBeenCalledTimes(1);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("real stranger verdict (no member identity) still takes the verdict path", () => {
|
|
616
|
+
const { resolved } = route(null, makeVerdict({ trustClass: "unknown" }));
|
|
617
|
+
|
|
618
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
619
|
+
expect(resolved.actorTrust.trustClass).toBe("unknown");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("valid member verdict (good status+policy) still takes the verdict path", () => {
|
|
623
|
+
const { outcome } = route(
|
|
624
|
+
null,
|
|
625
|
+
makeMemberVerdict("trusted_contact", {
|
|
626
|
+
status: "active",
|
|
627
|
+
policy: "allow",
|
|
628
|
+
}),
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
expect(resolveActorTrustMock).not.toHaveBeenCalled();
|
|
632
|
+
expect(outcome.action).toBe("normal_call");
|
|
633
|
+
});
|
|
634
|
+
});
|