@vellumai/assistant 0.11.3 → 0.11.4-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +139 -37
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +177 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/api/surfaces.ts +7 -3
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +19 -3
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +83 -12
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugin-api/vision-support.test.ts +1 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -123,11 +123,14 @@ describe("LLMSchema", () => {
|
|
|
123
123
|
expect(parsed.profileOrder).toEqual(["fast", "stale"]);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
-
test("
|
|
126
|
+
test("an unknown provider parses (read tolerance for entry names)", () => {
|
|
127
|
+
// The open provider schema keeps the profile; write-time membership is
|
|
128
|
+
// enforced at the profiles route and the config-write choke point, and
|
|
129
|
+
// dispatch resolves the value as a connection entry name.
|
|
127
130
|
const result = LLMSchema.safeParse({
|
|
128
131
|
profiles: { mine: { provider: "bogus-provider" } },
|
|
129
132
|
});
|
|
130
|
-
expect(result.success).toBe(
|
|
133
|
+
expect(result.success).toBe(true);
|
|
131
134
|
});
|
|
132
135
|
|
|
133
136
|
test("invalid temperature (negative) rejected", () => {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-level coverage for plugin-declared MCP servers appearing in
|
|
3
|
+
* `internal_mcp_list` (what `assistant mcp list` renders).
|
|
4
|
+
*
|
|
5
|
+
* The unit tests in `plugins/__tests__/mcp-servers.test.ts` cover reading
|
|
6
|
+
* `mcp.json`. This file covers the merge, and two properties that a
|
|
7
|
+
* refactor could quietly break:
|
|
8
|
+
*
|
|
9
|
+
* 1. A workspace entry of the same id wins. Getting precedence backwards
|
|
10
|
+
* would let a plugin redirect a server the user configured by hand.
|
|
11
|
+
* 2. A plugin server is never health-checked. `McpClient.connect` resolves
|
|
12
|
+
* `mcp:<serverId>:headers` and `mcp:<serverId>:tokens` from the
|
|
13
|
+
* credential store, and a plugin controls both its server key and its
|
|
14
|
+
* URL, so probing one would send a workspace credential to an endpoint
|
|
15
|
+
* the plugin chose whenever an id happens to match a stored key.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { beforeEach, describe, expect, jest, mock, test } from "bun:test";
|
|
21
|
+
|
|
22
|
+
const mockConnect = jest.fn();
|
|
23
|
+
const mockDisconnect = jest.fn();
|
|
24
|
+
/** Server ids the route actually constructed an MCP client for. */
|
|
25
|
+
const connectedServerIds: string[] = [];
|
|
26
|
+
|
|
27
|
+
mock.module("../mcp/client.js", () => ({
|
|
28
|
+
McpClient: class {
|
|
29
|
+
constructor(serverId: string) {
|
|
30
|
+
connectedServerIds.push(serverId);
|
|
31
|
+
}
|
|
32
|
+
get isConnected() {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
get lastError() {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
connect = mockConnect;
|
|
39
|
+
disconnect = mockDisconnect;
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../mcp/mcp-auth-orchestrator.js", () => ({
|
|
44
|
+
orchestrateMcpOAuthConnect: async () => ({
|
|
45
|
+
auth_url: "",
|
|
46
|
+
already_authenticated: false,
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
mock.module("../mcp/mcp-auth-state.js", () => ({
|
|
51
|
+
getMcpAuthState: () => null,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
mock.module("../mcp/mcp-oauth-provider.js", () => ({
|
|
55
|
+
// Stand in for a credential store that holds tokens for every id, which
|
|
56
|
+
// is the condition under which a leak would be observable.
|
|
57
|
+
hasMcpOAuthTokens: async () => true,
|
|
58
|
+
deleteMcpOAuthCredentials: async () => ({ ok: true, failedKeys: [] }),
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
mock.module("../mcp/mcp-header-store.js", () => ({
|
|
62
|
+
getMcpHeaders: async () => ({ Authorization: "Bearer workspace-secret" }),
|
|
63
|
+
setMcpHeaders: async () => {},
|
|
64
|
+
deleteMcpHeaders: async () => true,
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
mock.module("../daemon/mcp-reload-service.js", () => ({
|
|
68
|
+
reloadMcpServers: async () => {},
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
import { setConfig } from "./helpers/set-config.js";
|
|
72
|
+
|
|
73
|
+
setConfig("mcp", {
|
|
74
|
+
servers: {
|
|
75
|
+
"from-workspace": {
|
|
76
|
+
transport: { type: "streamable-http", url: "https://config.example/mcp" },
|
|
77
|
+
enabled: true,
|
|
78
|
+
defaultRiskLevel: "high",
|
|
79
|
+
maxTools: 20,
|
|
80
|
+
},
|
|
81
|
+
// Deliberately shares an id with the `shadowed` plugin below.
|
|
82
|
+
shadowed: {
|
|
83
|
+
transport: { type: "streamable-http", url: "https://wins.example/mcp" },
|
|
84
|
+
enabled: true,
|
|
85
|
+
defaultRiskLevel: "low",
|
|
86
|
+
maxTools: 20,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const { getWorkspacePluginsDir } = await import("../util/platform.js");
|
|
92
|
+
const { ROUTES } = await import("../runtime/routes/mcp-auth-routes.js");
|
|
93
|
+
|
|
94
|
+
const listHandler = ROUTES.find(
|
|
95
|
+
(r: { operationId: string }) => r.operationId === "internal_mcp_list",
|
|
96
|
+
)!.handler;
|
|
97
|
+
|
|
98
|
+
interface ListedServer {
|
|
99
|
+
id: string;
|
|
100
|
+
status: string;
|
|
101
|
+
source?: "workspace" | "plugin";
|
|
102
|
+
pluginName?: string;
|
|
103
|
+
defaultRiskLevel: string;
|
|
104
|
+
hasOAuth: boolean;
|
|
105
|
+
hasStaticAuth: boolean;
|
|
106
|
+
authType: string;
|
|
107
|
+
transport: { type: string; url?: string };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writePlugin(name: string, mcpJson: unknown): void {
|
|
111
|
+
const dir = join(getWorkspacePluginsDir(), name);
|
|
112
|
+
mkdirSync(dir, { recursive: true });
|
|
113
|
+
writeFileSync(
|
|
114
|
+
join(dir, "package.json"),
|
|
115
|
+
JSON.stringify({ name, version: "1.0.0" }),
|
|
116
|
+
);
|
|
117
|
+
writeFileSync(join(dir, "mcp.json"), JSON.stringify(mcpJson));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function unabyssManifest(): unknown {
|
|
121
|
+
return {
|
|
122
|
+
mcpServers: {
|
|
123
|
+
unabyss: { type: "streamable-http", url: "https://mcp.unabyss.com" },
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function listServers(): Promise<ListedServer[]> {
|
|
129
|
+
const result = (await listHandler({})) as { servers: ListedServer[] };
|
|
130
|
+
return result.servers;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
describe("internal_mcp_list, plugin-declared servers", () => {
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
mockConnect.mockReset();
|
|
136
|
+
mockDisconnect.mockReset();
|
|
137
|
+
connectedServerIds.length = 0;
|
|
138
|
+
rmSync(getWorkspacePluginsDir(), { recursive: true, force: true });
|
|
139
|
+
mkdirSync(getWorkspacePluginsDir(), { recursive: true });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("a plugin's mcp.json server appears alongside workspace servers", async () => {
|
|
143
|
+
writePlugin("unabyss", unabyssManifest());
|
|
144
|
+
|
|
145
|
+
const ids = (await listServers()).map((s) => s.id);
|
|
146
|
+
expect(ids).toContain("from-workspace");
|
|
147
|
+
expect(ids).toContain("unabyss");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("plugin servers are labelled with their origin, workspace servers are not", async () => {
|
|
151
|
+
writePlugin("unabyss", unabyssManifest());
|
|
152
|
+
|
|
153
|
+
const servers = await listServers();
|
|
154
|
+
const plugin = servers.find((s) => s.id === "unabyss")!;
|
|
155
|
+
const workspace = servers.find((s) => s.id === "from-workspace")!;
|
|
156
|
+
|
|
157
|
+
expect(plugin.source).toEqual("plugin");
|
|
158
|
+
expect(plugin.pluginName).toEqual("unabyss");
|
|
159
|
+
expect(workspace.source).toEqual("workspace");
|
|
160
|
+
expect(workspace.pluginName).toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("a workspace server of the same id wins over the plugin's", async () => {
|
|
164
|
+
writePlugin("shadowed", {
|
|
165
|
+
mcpServers: {
|
|
166
|
+
shadowed: { type: "streamable-http", url: "https://loses.example/mcp" },
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const matches = (await listServers()).filter((s) => s.id === "shadowed");
|
|
171
|
+
|
|
172
|
+
expect(matches).toHaveLength(1);
|
|
173
|
+
expect(matches[0].source).toEqual("workspace");
|
|
174
|
+
expect(matches[0].transport.url).toEqual("https://wins.example/mcp");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("plugin servers are not health-checked, so no credential can reach them", async () => {
|
|
178
|
+
writePlugin("unabyss", unabyssManifest());
|
|
179
|
+
|
|
180
|
+
const servers = await listServers();
|
|
181
|
+
|
|
182
|
+
expect(servers.find((s) => s.id === "unabyss")!.status).toEqual("declared");
|
|
183
|
+
// The credential mocks above return a token and an Authorization header
|
|
184
|
+
// for every id. Constructing a client for the plugin server is what
|
|
185
|
+
// would ship them to the plugin-declared URL.
|
|
186
|
+
expect(connectedServerIds).toContain("from-workspace");
|
|
187
|
+
expect(connectedServerIds).not.toContain("unabyss");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("plugin servers report no assistant-owned auth even when the store has some", async () => {
|
|
191
|
+
writePlugin("unabyss", unabyssManifest());
|
|
192
|
+
|
|
193
|
+
const plugin = (await listServers()).find((s) => s.id === "unabyss")!;
|
|
194
|
+
expect(plugin.hasOAuth).toBe(false);
|
|
195
|
+
expect(plugin.hasStaticAuth).toBe(false);
|
|
196
|
+
expect(plugin.authType).toEqual("none");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("workspace servers keep reporting their auth state", async () => {
|
|
200
|
+
const workspace = (await listServers()).find(
|
|
201
|
+
(s) => s.id === "from-workspace",
|
|
202
|
+
)!;
|
|
203
|
+
expect(workspace.hasOAuth).toBe(true);
|
|
204
|
+
expect(workspace.hasStaticAuth).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("plugin servers default to low risk", async () => {
|
|
208
|
+
writePlugin("unabyss", unabyssManifest());
|
|
209
|
+
|
|
210
|
+
const servers = await listServers();
|
|
211
|
+
expect(servers.find((s) => s.id === "unabyss")!.defaultRiskLevel).toEqual(
|
|
212
|
+
"low",
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("a directory with no valid package.json is not advertised", async () => {
|
|
217
|
+
// The runtime loader would never load it, and its stdio command must
|
|
218
|
+
// not be reachable through the listing.
|
|
219
|
+
const dir = join(getWorkspacePluginsDir(), "no-manifest");
|
|
220
|
+
mkdirSync(dir, { recursive: true });
|
|
221
|
+
writeFileSync(
|
|
222
|
+
join(dir, "mcp.json"),
|
|
223
|
+
JSON.stringify({
|
|
224
|
+
mcpServers: { evil: { type: "stdio", command: "touch" } },
|
|
225
|
+
}),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const ids = (await listServers()).map((s) => s.id);
|
|
229
|
+
expect(ids).not.toContain("no-manifest__evil");
|
|
230
|
+
expect(ids).toContain("from-workspace");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("a malformed plugin manifest does not remove workspace servers", async () => {
|
|
234
|
+
const dir = join(getWorkspacePluginsDir(), "broken");
|
|
235
|
+
mkdirSync(dir, { recursive: true });
|
|
236
|
+
writeFileSync(
|
|
237
|
+
join(dir, "package.json"),
|
|
238
|
+
JSON.stringify({ name: "broken", version: "1.0.0" }),
|
|
239
|
+
);
|
|
240
|
+
writeFileSync(join(dir, "mcp.json"), "{ not json");
|
|
241
|
+
|
|
242
|
+
const ids = (await listServers()).map((s) => s.id);
|
|
243
|
+
expect(ids).toContain("from-workspace");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("no plugins installed leaves the listing unchanged", async () => {
|
|
247
|
+
const servers = await listServers();
|
|
248
|
+
expect(servers.every((s) => s.source === "workspace")).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -73,6 +73,7 @@ import type { QdrantSparseVector } from "../persistence/embeddings/qdrant-client
|
|
|
73
73
|
import type { ConversationGraphMemory } from "../plugins/defaults/memory/graph/conversation-graph-memory.js";
|
|
74
74
|
import userPromptSubmitMemoryRetrieval from "../plugins/defaults/memory/hooks/user-prompt-submit.js";
|
|
75
75
|
import type { Message } from "../providers/types.js";
|
|
76
|
+
import { asConversation } from "./helpers/mock-conversation.js";
|
|
76
77
|
|
|
77
78
|
/** Canonical metrics payload the graph retriever attaches to a real hit. */
|
|
78
79
|
function makeMetrics() {
|
|
@@ -172,15 +173,15 @@ function installConversation(
|
|
|
172
173
|
graphMemory: ConversationGraphMemory,
|
|
173
174
|
opts?: {
|
|
174
175
|
trusted?: boolean;
|
|
175
|
-
|
|
176
|
+
abortController?: AbortController;
|
|
176
177
|
},
|
|
177
178
|
): void {
|
|
178
179
|
currentTrustClass = opts?.trusted === false ? "unknown" : "guardian";
|
|
179
|
-
currentConversation = {
|
|
180
|
+
currentConversation = asConversation({
|
|
180
181
|
graphMemory,
|
|
181
182
|
trustContext: undefined,
|
|
182
|
-
abortController:
|
|
183
|
-
}
|
|
183
|
+
abortController: opts?.abortController ?? new AbortController(),
|
|
184
|
+
});
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
beforeEach(() => {
|
|
@@ -492,7 +493,7 @@ describe("user-prompt-submit hook (memory retrieval)", () => {
|
|
|
492
493
|
const controller = new AbortController();
|
|
493
494
|
installConversation(graphMemory, {
|
|
494
495
|
trusted: true,
|
|
495
|
-
|
|
496
|
+
abortController: controller,
|
|
496
497
|
});
|
|
497
498
|
const ctx = makeHookCtx();
|
|
498
499
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import { describe, expect, test } from "bun:test";
|
|
4
|
+
|
|
5
|
+
import { Glob } from "bun";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Ratchet over direct reads of the `messages` table outside `persistence/`.
|
|
9
|
+
*
|
|
10
|
+
* Every row in `messages` is conversation content in some state of
|
|
11
|
+
* completeness, and each read encodes a visibility decision (completed
|
|
12
|
+
* history vs. any row). `persistence/` owns that contract: named accessors in
|
|
13
|
+
* `message-reads.ts` and the history readers in `conversation-crud.ts` state
|
|
14
|
+
* their decision in their name; everything else should go through them. A
|
|
15
|
+
* direct reader that re-decides visibility on its own is how the contract
|
|
16
|
+
* fails silently, so a new direct read is a reviewed exception, not a
|
|
17
|
+
* default.
|
|
18
|
+
*
|
|
19
|
+
* The BASELINE lists every file outside `persistence/` that reads the table
|
|
20
|
+
* directly today. Each entry has been audited and carries its visibility
|
|
21
|
+
* decision as a comment at the read site. The guard fails when a file outside
|
|
22
|
+
* the baseline gains a direct read (route it through an accessor, or audit it
|
|
23
|
+
* and baseline it with a site comment explaining the decision in your PR),
|
|
24
|
+
* and when a baseline entry goes stale (remove it, so coverage cannot rot).
|
|
25
|
+
*
|
|
26
|
+
* Detection is deliberately shape-based (from/join of `messages` in drizzle
|
|
27
|
+
* builders, `FROM messages` / `JOIN messages` in raw SQL): this is a ratchet
|
|
28
|
+
* against sprawl, not a proof, and matching the read shapes keeps it free of
|
|
29
|
+
* false positives from unrelated identifiers.
|
|
30
|
+
*
|
|
31
|
+
* Regenerate after an intentional change with:
|
|
32
|
+
* UPDATE_MESSAGES_READ_BASELINE=1 bun test src/__tests__/messages-read-boundary-guard.test.ts
|
|
33
|
+
* and paste the printed list here, explaining the new read in your PR.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const SRC_REL = "src";
|
|
37
|
+
const SRC_ABS = join(process.cwd(), SRC_REL);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Audited direct readers outside persistence/, as file -> count of matching
|
|
41
|
+
* read sites. Counts, not just filenames: a new read added inside an
|
|
42
|
+
* already-baselined file must fail the ratchet too, since each site carries
|
|
43
|
+
* its own visibility decision. A same-file refactor that replaces one read
|
|
44
|
+
* with another at equal count passes; that is accepted ratchet granularity
|
|
45
|
+
* (the diff still shows the site to its reviewer).
|
|
46
|
+
*/
|
|
47
|
+
const BASELINE: Readonly<Record<string, number>> = {
|
|
48
|
+
"src/apps/app-store.ts": 1,
|
|
49
|
+
"src/daemon/credential-transcript-scrub.ts": 1,
|
|
50
|
+
"src/live-voice/live-voice-archive.ts": 1,
|
|
51
|
+
"src/monitoring/recovery/inflight-content.ts": 2,
|
|
52
|
+
"src/plugins/defaults/memory/context-search/sources/conversations.ts": 1,
|
|
53
|
+
"src/plugins/defaults/memory/graph/image-ref-utils.ts": 1,
|
|
54
|
+
"src/plugins/defaults/memory/indexer.ts": 2,
|
|
55
|
+
"src/plugins/defaults/memory/memory-retrospective-accounting.ts": 4,
|
|
56
|
+
"src/plugins/defaults/memory/substrate/sweep-job.ts": 1,
|
|
57
|
+
"src/plugins/defaults/memory/v1/graph/extraction.ts": 3,
|
|
58
|
+
"src/plugins/defaults/memory/v1/job-handlers/backfill.ts": 1,
|
|
59
|
+
"src/plugins/defaults/memory/v1/job-handlers/embedding.ts": 1,
|
|
60
|
+
"src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts": 1,
|
|
61
|
+
"src/plugins/defaults/memory/v2/harness/oracle.ts": 1,
|
|
62
|
+
"src/plugins/defaults/memory/v2/harness/replay-input.ts": 1,
|
|
63
|
+
"src/plugins/defaults/memory/v3-eval/eval-packets.ts": 2,
|
|
64
|
+
"src/plugins/defaults/memory/v3/prune.ts": 1,
|
|
65
|
+
"src/plugins/defaults/memory/v3/selection-log-store.ts": 1,
|
|
66
|
+
"src/runtime/pre-first-message-gate.ts": 1,
|
|
67
|
+
"src/runtime/routes/log-export-routes.ts": 1,
|
|
68
|
+
"src/runtime/routes/surface-conversation-resolver.ts": 2,
|
|
69
|
+
"src/telemetry/turn-events-store.ts": 2,
|
|
70
|
+
"src/telemetry/turn-trace-store.ts": 2,
|
|
71
|
+
"src/workspace/migrations/rebuild-conversation-disk-view.ts": 1,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const DRIZZLE_READ =
|
|
75
|
+
/\.(?:from|innerJoin|leftJoin|rightJoin|fullJoin)\(\s*messages\s*[,)]/g;
|
|
76
|
+
const RAW_SQL_READ = /(?:FROM|JOIN)\s+messages\b/g;
|
|
77
|
+
|
|
78
|
+
function isExempt(relPath: string): boolean {
|
|
79
|
+
const parts = relPath.split(sep);
|
|
80
|
+
return (
|
|
81
|
+
parts[1] === "persistence" ||
|
|
82
|
+
parts.includes("__tests__") ||
|
|
83
|
+
relPath.endsWith(".test.ts")
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function scanDirectReaders(): Promise<Map<string, number>> {
|
|
88
|
+
const glob = new Glob("**/*.ts");
|
|
89
|
+
const found = new Map<string, number>();
|
|
90
|
+
for await (const file of glob.scan({ cwd: SRC_ABS, absolute: true })) {
|
|
91
|
+
const relPath = join(SRC_REL, relative(SRC_ABS, file));
|
|
92
|
+
if (isExempt(relPath)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const source = readFileSync(file, "utf-8");
|
|
96
|
+
const count =
|
|
97
|
+
[...source.matchAll(DRIZZLE_READ)].length +
|
|
98
|
+
[...source.matchAll(RAW_SQL_READ)].length;
|
|
99
|
+
if (count > 0) {
|
|
100
|
+
found.set(relPath, count);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return found;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
describe("messages read boundary", () => {
|
|
107
|
+
test("no new direct messages reads beyond the audited baseline", async () => {
|
|
108
|
+
const found = await scanDirectReaders();
|
|
109
|
+
|
|
110
|
+
if (process.env.UPDATE_MESSAGES_READ_BASELINE === "1") {
|
|
111
|
+
console.log(
|
|
112
|
+
JSON.stringify(Object.fromEntries([...found].sort()), null, 2),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const grown = [...found]
|
|
117
|
+
.filter(([file, count]) => count > (BASELINE[file] ?? 0))
|
|
118
|
+
.map(([file, count]) => `${file}: ${BASELINE[file] ?? 0} -> ${count}`);
|
|
119
|
+
expect(
|
|
120
|
+
grown,
|
|
121
|
+
`New direct reads of the messages table outside persistence/ (route ` +
|
|
122
|
+
`through an accessor in persistence/message-reads.ts, or audit the ` +
|
|
123
|
+
`read, state its visibility decision at the site, and baseline it):`,
|
|
124
|
+
).toEqual([]);
|
|
125
|
+
|
|
126
|
+
const shrunk = Object.entries(BASELINE)
|
|
127
|
+
.filter(([file, count]) => (found.get(file) ?? 0) < count)
|
|
128
|
+
.map(([file, count]) => `${file}: ${count} -> ${found.get(file) ?? 0}`);
|
|
129
|
+
expect(
|
|
130
|
+
shrunk,
|
|
131
|
+
"Stale baseline entries (fewer direct reads than baselined; ratchet the count down so coverage cannot rot):",
|
|
132
|
+
).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -30,10 +30,10 @@ import {
|
|
|
30
30
|
} from "bun:test";
|
|
31
31
|
|
|
32
32
|
import { _inspectHookCacheForTests } from "../hooks/hook-loader.js";
|
|
33
|
+
import { getUserHooksFor } from "../hooks/registry.js";
|
|
33
34
|
import {
|
|
34
35
|
_inspectToolCacheForTests,
|
|
35
36
|
getCachedUserTools,
|
|
36
|
-
getUserHooksFor,
|
|
37
37
|
populateCacheAtBoot,
|
|
38
38
|
reconcilePluginSourcesNow,
|
|
39
39
|
resetPluginCacheForTests,
|
|
@@ -501,7 +501,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
501
501
|
|
|
502
502
|
test("notifyGuardianOfAccessRequest returns no_sender_id when actorExternalId is absent", async () => {
|
|
503
503
|
const result = await notifyGuardianOfAccessRequest({
|
|
504
|
-
canonicalAssistantId: "self",
|
|
505
504
|
sourceChannel: "telegram",
|
|
506
505
|
conversationExternalId: "chat-123",
|
|
507
506
|
actorExternalId: undefined,
|
|
@@ -522,7 +521,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
522
521
|
|
|
523
522
|
test("notifyGuardianOfAccessRequest creates request with self-healed principal when no binding exists", async () => {
|
|
524
523
|
const result = await notifyGuardianOfAccessRequest({
|
|
525
|
-
canonicalAssistantId: "self",
|
|
526
524
|
sourceChannel: "telegram",
|
|
527
525
|
conversationExternalId: "chat-123",
|
|
528
526
|
actorExternalId: "unknown-user",
|
|
@@ -565,7 +563,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
565
563
|
});
|
|
566
564
|
|
|
567
565
|
const result = await notifyGuardianOfAccessRequest({
|
|
568
|
-
canonicalAssistantId: "self",
|
|
569
566
|
sourceChannel: "telegram",
|
|
570
567
|
conversationExternalId: "tg-chat",
|
|
571
568
|
actorExternalId: "unknown-tg-user",
|
|
@@ -619,7 +616,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
619
616
|
});
|
|
620
617
|
|
|
621
618
|
const result = await notifyGuardianOfAccessRequest({
|
|
622
|
-
canonicalAssistantId: "self",
|
|
623
619
|
sourceChannel: "telegram",
|
|
624
620
|
conversationExternalId: "chat-123",
|
|
625
621
|
actorExternalId: "unknown-user",
|
|
@@ -660,7 +656,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
660
656
|
});
|
|
661
657
|
|
|
662
658
|
const result = await notifyGuardianOfAccessRequest({
|
|
663
|
-
canonicalAssistantId: "self",
|
|
664
659
|
sourceChannel: "telegram",
|
|
665
660
|
conversationExternalId: "chat-123",
|
|
666
661
|
actorExternalId: "unknown-user",
|
|
@@ -688,7 +683,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
688
683
|
test("notifyGuardianOfAccessRequest resolves the vellum anchor from the gateway delivery", async () => {
|
|
689
684
|
// Only the vellum anchor (seeded in resetState) is served by the gateway.
|
|
690
685
|
const result = await notifyGuardianOfAccessRequest({
|
|
691
|
-
canonicalAssistantId: "self",
|
|
692
686
|
sourceChannel: "telegram",
|
|
693
687
|
conversationExternalId: "chat-123",
|
|
694
688
|
actorExternalId: "unknown-user",
|
|
@@ -722,7 +716,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
722
716
|
|
|
723
717
|
await expect(
|
|
724
718
|
notifyGuardianOfAccessRequest({
|
|
725
|
-
canonicalAssistantId: "self",
|
|
726
719
|
sourceChannel: "telegram",
|
|
727
720
|
conversationExternalId: "chat-123",
|
|
728
721
|
actorExternalId: "unknown-user",
|
|
@@ -739,7 +732,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
739
732
|
|
|
740
733
|
test("notifyGuardianOfAccessRequest for voice channel includes actorDisplayName in contextPayload", async () => {
|
|
741
734
|
const result = await notifyGuardianOfAccessRequest({
|
|
742
|
-
canonicalAssistantId: "self",
|
|
743
735
|
sourceChannel: "phone",
|
|
744
736
|
conversationExternalId: "+15559998888",
|
|
745
737
|
actorExternalId: "+15559998888",
|
|
@@ -770,7 +762,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
770
762
|
|
|
771
763
|
test("notifyGuardianOfAccessRequest includes requestCode in contextPayload", async () => {
|
|
772
764
|
const result = await notifyGuardianOfAccessRequest({
|
|
773
|
-
canonicalAssistantId: "self",
|
|
774
765
|
sourceChannel: "telegram",
|
|
775
766
|
conversationExternalId: "chat-123",
|
|
776
767
|
actorExternalId: "unknown-user",
|
|
@@ -794,7 +785,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
794
785
|
// is created and the status is forwarded to the card payload. (A `revoked`
|
|
795
786
|
// status would instead suppress — covered by the keep-out tests.)
|
|
796
787
|
const result = await notifyGuardianOfAccessRequest({
|
|
797
|
-
canonicalAssistantId: "self",
|
|
798
788
|
sourceChannel: "telegram",
|
|
799
789
|
conversationExternalId: "chat-123",
|
|
800
790
|
actorExternalId: "pending-user",
|
|
@@ -834,7 +824,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
834
824
|
};
|
|
835
825
|
|
|
836
826
|
const result = await notifyGuardianOfAccessRequest({
|
|
837
|
-
canonicalAssistantId: "self",
|
|
838
827
|
sourceChannel: "phone",
|
|
839
828
|
conversationExternalId: "+15556667777",
|
|
840
829
|
actorExternalId: "+15556667777",
|
|
@@ -897,7 +886,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
897
886
|
};
|
|
898
887
|
|
|
899
888
|
const result = await notifyGuardianOfAccessRequest({
|
|
900
|
-
canonicalAssistantId: "self",
|
|
901
889
|
sourceChannel: "telegram",
|
|
902
890
|
conversationExternalId: "chat-123",
|
|
903
891
|
actorExternalId: "unknown-user",
|
|
@@ -929,7 +917,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
929
917
|
// request row. A revoked/blocked contact is deliberately kept out — no new
|
|
930
918
|
// prompt, signal, or pending request.
|
|
931
919
|
const result = await notifyGuardianOfAccessRequest({
|
|
932
|
-
canonicalAssistantId: "self",
|
|
933
920
|
sourceChannel: "telegram",
|
|
934
921
|
conversationExternalId: "chat-kept-out",
|
|
935
922
|
actorExternalId: "revoked-user",
|
|
@@ -969,7 +956,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
969
956
|
});
|
|
970
957
|
|
|
971
958
|
const result = await notifyGuardianOfAccessRequest({
|
|
972
|
-
canonicalAssistantId: "self",
|
|
973
959
|
sourceChannel: "telegram",
|
|
974
960
|
conversationExternalId: "chat-parked",
|
|
975
961
|
actorExternalId: "parked-user",
|
|
@@ -1006,7 +992,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
1006
992
|
});
|
|
1007
993
|
|
|
1008
994
|
const result = await notifyGuardianOfAccessRequest({
|
|
1009
|
-
canonicalAssistantId: "self",
|
|
1010
995
|
sourceChannel: "telegram",
|
|
1011
996
|
conversationExternalId: "chat-approved",
|
|
1012
997
|
actorExternalId: "approved-user",
|
|
@@ -1047,7 +1032,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
1047
1032
|
bridgeState.requests.get(requestId)!.updatedAt = staleUpdatedAt;
|
|
1048
1033
|
|
|
1049
1034
|
const result = await notifyGuardianOfAccessRequest({
|
|
1050
|
-
canonicalAssistantId: "self",
|
|
1051
1035
|
sourceChannel: "telegram",
|
|
1052
1036
|
conversationExternalId: "chat-stale",
|
|
1053
1037
|
actorExternalId: "stale-user",
|
|
@@ -1082,7 +1066,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
1082
1066
|
});
|
|
1083
1067
|
|
|
1084
1068
|
const result = await notifyGuardianOfAccessRequest({
|
|
1085
|
-
canonicalAssistantId: "self",
|
|
1086
1069
|
sourceChannel: "telegram",
|
|
1087
1070
|
conversationExternalId: "chat-flip",
|
|
1088
1071
|
actorExternalId: "flip-user",
|
|
@@ -1101,7 +1084,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
1101
1084
|
// Suppression is scoped to the sender via their own contact status, so a
|
|
1102
1085
|
// kept-out contact never bleeds into a different sender's prompt.
|
|
1103
1086
|
const keptOut = await notifyGuardianOfAccessRequest({
|
|
1104
|
-
canonicalAssistantId: "self",
|
|
1105
1087
|
sourceChannel: "telegram",
|
|
1106
1088
|
conversationExternalId: "chat-kept",
|
|
1107
1089
|
actorExternalId: "revoked-user",
|
|
@@ -1112,7 +1094,6 @@ describe("access-request-helper unit tests", () => {
|
|
|
1112
1094
|
|
|
1113
1095
|
// A different sender with no keep-out still gets a fresh prompt.
|
|
1114
1096
|
const result = await notifyGuardianOfAccessRequest({
|
|
1115
|
-
canonicalAssistantId: "self",
|
|
1116
1097
|
sourceChannel: "telegram",
|
|
1117
1098
|
conversationExternalId: "chat-fresh",
|
|
1118
1099
|
actorExternalId: "fresh-user",
|
|
@@ -1141,7 +1122,6 @@ describe("maybeNotifyGuardianOfAdmittedContact", () => {
|
|
|
1141
1122
|
});
|
|
1142
1123
|
|
|
1143
1124
|
const baseParams = {
|
|
1144
|
-
canonicalAssistantId: "self",
|
|
1145
1125
|
sourceChannel: "telegram" as const,
|
|
1146
1126
|
conversationExternalId: "chat-123",
|
|
1147
1127
|
actorExternalId: "user-unknown-456",
|
|
@@ -103,7 +103,10 @@ mock.module("../persistence/conversation-crud.js", () => ({
|
|
|
103
103
|
},
|
|
104
104
|
// The handler treats provenance as a flat spread; returning {} keeps the
|
|
105
105
|
// metadata snapshot focused on the fields under test.
|
|
106
|
-
|
|
106
|
+
// Mirrors the real mapping so provenance assertions observe what production
|
|
107
|
+
// stamps rather than a placeholder.
|
|
108
|
+
provenanceFromTrustContext: (ctx?: { trustClass?: string }) =>
|
|
109
|
+
ctx ? { provenanceTrustClass: ctx.trustClass } : {},
|
|
107
110
|
reserveMessage: mock(
|
|
108
111
|
async (
|
|
109
112
|
conversationId: string,
|
|
@@ -187,6 +190,8 @@ function makeDeps(
|
|
|
187
190
|
requesterTimezoneLabel?: string;
|
|
188
191
|
clientTimezone?: string;
|
|
189
192
|
sourceThreadId?: string;
|
|
193
|
+
/** Turn-local trust; when set, the live slot below diverges from it. */
|
|
194
|
+
currentTurnTrustContext?: Record<string, unknown>;
|
|
190
195
|
} = {},
|
|
191
196
|
): EventHandlerDeps {
|
|
192
197
|
const assistantMessageChannel = overrides.assistantMessageChannel ?? "slack";
|
|
@@ -195,6 +200,7 @@ function makeDeps(
|
|
|
195
200
|
conversationId,
|
|
196
201
|
provider: { name: "anthropic" },
|
|
197
202
|
currentTurnSurfaces: [],
|
|
203
|
+
currentTurnTrustContext: overrides.currentTurnTrustContext,
|
|
198
204
|
trustContext: {
|
|
199
205
|
sourceChannel: assistantMessageChannel,
|
|
200
206
|
trustClass: "guardian",
|
|
@@ -439,6 +445,39 @@ describe("outbound assistant Slack metadata persistence", () => {
|
|
|
439
445
|
expect(slackMeta.channelTs).toBeUndefined();
|
|
440
446
|
});
|
|
441
447
|
|
|
448
|
+
test("envelope resolves from the turn's actor when the slot has moved", async () => {
|
|
449
|
+
// Provenance class and Slack routing must come from ONE actor: the turn's.
|
|
450
|
+
// A queued Slack turn runs while the guardian has since written the live
|
|
451
|
+
// slot; the assistant row must carry the sender's class AND the sender's
|
|
452
|
+
// channel/thread, not a mixed envelope.
|
|
453
|
+
const conversationId = "conv-slack-moved-slot";
|
|
454
|
+
const deps = makeDeps(conversationId, {
|
|
455
|
+
assistantMessageChannel: "slack",
|
|
456
|
+
// Live slot: the guardian, on a different chat, no thread.
|
|
457
|
+
requesterChatId: "C-GUARDIAN",
|
|
458
|
+
currentTurnTrustContext: {
|
|
459
|
+
sourceChannel: "slack",
|
|
460
|
+
trustClass: "trusted_contact",
|
|
461
|
+
requesterChatId: "C-CONTACT",
|
|
462
|
+
sourceThreadId: "1723300000.000100",
|
|
463
|
+
},
|
|
464
|
+
});
|
|
465
|
+
await handleLlmCallStarted(state, deps);
|
|
466
|
+
await handleMessageComplete(
|
|
467
|
+
state,
|
|
468
|
+
deps,
|
|
469
|
+
makeMessageCompleteEvent("reply to the contact"),
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
const persisted = lastAssistantPersisted();
|
|
473
|
+
expect(persisted.metadata?.provenanceTrustClass).toBe("trusted_contact");
|
|
474
|
+
const slackMeta = JSON.parse(
|
|
475
|
+
persisted.metadata?.slackMeta as string,
|
|
476
|
+
) as Record<string, unknown>;
|
|
477
|
+
expect(slackMeta.channelId).toBe("C-CONTACT");
|
|
478
|
+
expect(slackMeta.threadTs).toBe("1723300000.000100");
|
|
479
|
+
});
|
|
480
|
+
|
|
442
481
|
test("ignores a non-ts sourceThreadId", async () => {
|
|
443
482
|
const conversationId = "conv-slack-bad-thread";
|
|
444
483
|
const channelId = "C789BAD";
|