@vellumai/assistant 0.11.4 → 0.11.5-staging.1
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/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/feature-flag-registry.json +35 -19
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the conversation-scoped shape of `GET /v1/pending-interactions`.
|
|
3
|
+
*
|
|
4
|
+
* The registry is the authority clients project their prompt UI from, so what
|
|
5
|
+
* this route says about a kind matters as much when nothing is outstanding as
|
|
6
|
+
* when something is. Covers:
|
|
7
|
+
* - an outstanding question is reported with its full batch,
|
|
8
|
+
* - no question reports `null` (the value that authorizes a client to retire
|
|
9
|
+
* a card), not an omitted key,
|
|
10
|
+
* - every conversation-scoped path names the key, including the early return
|
|
11
|
+
* for a conversation that cannot be resolved,
|
|
12
|
+
* - the newest question wins while a superseded one is still settling,
|
|
13
|
+
* - a question registered without entries is not reportable,
|
|
14
|
+
* - the diagnostic (unfiltered) mode is unchanged.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
18
|
+
|
|
19
|
+
import type { QuestionEntry } from "../../../api/events/question-request.js";
|
|
20
|
+
import * as pendingInteractions from "../../pending-interactions.js";
|
|
21
|
+
import type { RouteDefinition, RouteHandlerArgs } from "../types.js";
|
|
22
|
+
|
|
23
|
+
// The conversation-key lookup is the only path into the unresolvable-conversation
|
|
24
|
+
// early return, and it reads SQLite. Stub it so this stays a unit test of the
|
|
25
|
+
// response shape. Imported dynamically below so the stub is in place first.
|
|
26
|
+
mock.module("../../../persistence/conversation-key-store.js", () => ({
|
|
27
|
+
getConversationByKey: () => undefined,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
const { ROUTES: APPROVAL_ROUTES } = await import("../approval-routes.js");
|
|
31
|
+
|
|
32
|
+
function findHandler(operationId: string): RouteDefinition["handler"] {
|
|
33
|
+
const route = APPROVAL_ROUTES.find((r) => r.operationId === operationId);
|
|
34
|
+
if (!route) {
|
|
35
|
+
throw new Error(`Route ${operationId} not found`);
|
|
36
|
+
}
|
|
37
|
+
return route.handler;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const handler = findHandler("pending_interactions");
|
|
41
|
+
|
|
42
|
+
interface ScopedResponse {
|
|
43
|
+
pendingConfirmation: unknown;
|
|
44
|
+
pendingSecret: unknown;
|
|
45
|
+
pendingQuestion: { requestId: string; entries: QuestionEntry[] } | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function listForConversation(
|
|
49
|
+
conversationId: string,
|
|
50
|
+
): Promise<ScopedResponse> {
|
|
51
|
+
const result = await handler({
|
|
52
|
+
queryParams: { conversationId },
|
|
53
|
+
} as unknown as RouteHandlerArgs);
|
|
54
|
+
return result as ScopedResponse;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ENTRIES: QuestionEntry[] = [
|
|
58
|
+
{
|
|
59
|
+
id: "q1",
|
|
60
|
+
question: "Which draft should I send?",
|
|
61
|
+
options: [
|
|
62
|
+
{ id: "a", label: "The short one" },
|
|
63
|
+
{ id: "b", label: "The long one" },
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function registerQuestion(requestId: string, entries = ENTRIES): void {
|
|
69
|
+
pendingInteractions.register(requestId, {
|
|
70
|
+
conversationId: "conv-1",
|
|
71
|
+
kind: "question",
|
|
72
|
+
toolUseId: `tool-${requestId}`,
|
|
73
|
+
questionDetails: { entries },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** A question interaction that never got its batch attached. */
|
|
78
|
+
function registerQuestionWithoutDetails(requestId: string): void {
|
|
79
|
+
pendingInteractions.register(requestId, {
|
|
80
|
+
conversationId: "conv-1",
|
|
81
|
+
kind: "question",
|
|
82
|
+
toolUseId: `tool-${requestId}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
pendingInteractions.clear();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("pending-interactions, conversation-scoped", () => {
|
|
91
|
+
test("reports an outstanding question with its full batch", async () => {
|
|
92
|
+
// GIVEN a question interaction awaiting an answer in the conversation
|
|
93
|
+
registerQuestion("req-1");
|
|
94
|
+
|
|
95
|
+
// WHEN the conversation's pending interactions are read
|
|
96
|
+
const result = await listForConversation("conv-1");
|
|
97
|
+
|
|
98
|
+
// THEN the prompt is reported with everything the card needs to render
|
|
99
|
+
expect(result.pendingQuestion).toEqual({
|
|
100
|
+
requestId: "req-1",
|
|
101
|
+
entries: ENTRIES,
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("reports null once the question is resolved", async () => {
|
|
106
|
+
// GIVEN a question that has since been answered (registry entry consumed)
|
|
107
|
+
registerQuestion("req-1");
|
|
108
|
+
pendingInteractions.resolve("req-1", "answered");
|
|
109
|
+
|
|
110
|
+
// WHEN the conversation's pending interactions are read
|
|
111
|
+
const result = await listForConversation("conv-1");
|
|
112
|
+
|
|
113
|
+
// THEN the answer is a positive "nothing outstanding", which is what lets a
|
|
114
|
+
// client retire a card it is still showing. An omitted key would instead
|
|
115
|
+
// read as "this assistant has no opinion" and strand the card.
|
|
116
|
+
expect(result.pendingQuestion).toBeNull();
|
|
117
|
+
expect("pendingQuestion" in result).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("names the key even when the conversation cannot be resolved", async () => {
|
|
121
|
+
// GIVEN a conversationKey that maps to no conversation
|
|
122
|
+
const result = (await handler({
|
|
123
|
+
queryParams: { conversationKey: "no-such-key" },
|
|
124
|
+
} as unknown as RouteHandlerArgs)) as ScopedResponse;
|
|
125
|
+
|
|
126
|
+
// THEN the early return still states all three kinds, so this path is not
|
|
127
|
+
// mistaken for an assistant that predates the field
|
|
128
|
+
expect("pendingQuestion" in result).toBe(true);
|
|
129
|
+
expect(result.pendingQuestion).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("reports the newest question while a superseded one settles", async () => {
|
|
133
|
+
// GIVEN a superseded prompt that has not finished settling through its
|
|
134
|
+
// abort signal, so both entries sit in the registry at once
|
|
135
|
+
const newerEntries: QuestionEntry[] = [
|
|
136
|
+
{ id: "q1", question: "Actually, which account?", options: [] },
|
|
137
|
+
];
|
|
138
|
+
registerQuestion("req-old");
|
|
139
|
+
registerQuestion("req-new", newerEntries);
|
|
140
|
+
|
|
141
|
+
// WHEN the conversation's pending interactions are read
|
|
142
|
+
const result = await listForConversation("conv-1");
|
|
143
|
+
|
|
144
|
+
// THEN the prompt the user is actually being asked is the one reported
|
|
145
|
+
expect(result.pendingQuestion?.requestId).toBe("req-new");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("does not report a question registered without its entries", async () => {
|
|
149
|
+
// GIVEN a question interaction carrying no renderable batch
|
|
150
|
+
registerQuestionWithoutDetails("req-1");
|
|
151
|
+
|
|
152
|
+
// WHEN the conversation's pending interactions are read
|
|
153
|
+
const result = await listForConversation("conv-1");
|
|
154
|
+
|
|
155
|
+
// THEN there is nothing a client could raise
|
|
156
|
+
expect(result.pendingQuestion).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("leaves the unfiltered diagnostic listing unchanged", async () => {
|
|
160
|
+
// GIVEN pending interactions across the daemon
|
|
161
|
+
registerQuestion("req-1");
|
|
162
|
+
|
|
163
|
+
// WHEN the route is called with no conversation filter
|
|
164
|
+
const result = (await handler({
|
|
165
|
+
queryParams: {},
|
|
166
|
+
} as unknown as RouteHandlerArgs)) as {
|
|
167
|
+
interactions: Array<{ requestId: string; kind: string }>;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// THEN it still answers with the flat cross-conversation list
|
|
171
|
+
expect(result.interactions).toEqual([
|
|
172
|
+
expect.objectContaining({ requestId: "req-1", kind: "question" }),
|
|
173
|
+
]);
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -38,9 +38,12 @@
|
|
|
38
38
|
* `plugins:list` tag via the canonical resource-sync publisher (enable and
|
|
39
39
|
* disable emit the SAME invalidation)
|
|
40
40
|
* - Threads `x-vellum-client-id` into the published event's `originClientId`
|
|
41
|
-
* -
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* - Enable pokes the imperative plugin source reconcile, which activates a
|
|
42
|
+
* plugin the boot scan skipped for its sentinel and converges its
|
|
43
|
+
* schedules and MCP servers
|
|
44
|
+
* - Disable pokes the plugin-declared schedule reconcile on a successful
|
|
45
|
+
* toggle (and not on a failed one), so the plugin's rows disarm
|
|
46
|
+
* immediately instead of at the reconciler's next backstop sweep
|
|
44
47
|
* - A broadcast failure does not fail a successful toggle (the publisher
|
|
45
48
|
* swallows hub errors)
|
|
46
49
|
* - Maps `InvalidPluginNameError` → BadRequestError (400)
|
|
@@ -316,8 +319,8 @@ mock.module("../../../cli/lib/toggle-plugin.js", () => ({
|
|
|
316
319
|
enablePlugin: enablePluginSpy,
|
|
317
320
|
}));
|
|
318
321
|
|
|
319
|
-
// Spy on the plugin-schedule reconcile the
|
|
320
|
-
//
|
|
322
|
+
// Spy on the plugin-schedule reconcile the disable handler pokes. The handler
|
|
323
|
+
// imports it lazily at call time, so this mock intercepts the dynamic import
|
|
321
324
|
// and keeps the reconciler's persistence graph out of the test.
|
|
322
325
|
const reconcilePluginSchedulesSpy = mock(() => Promise.resolve());
|
|
323
326
|
|
|
@@ -325,6 +328,21 @@ mock.module("../../../schedule/plugin-schedule-reconciler.js", () => ({
|
|
|
325
328
|
reconcilePluginSchedules: reconcilePluginSchedulesSpy,
|
|
326
329
|
}));
|
|
327
330
|
|
|
331
|
+
// Spy on the imperative plugin source reconcile so the enable route's
|
|
332
|
+
// activation poke is assertable without scanning a real plugins tree. Only
|
|
333
|
+
// that one export is swapped; the rest of the module passes through real,
|
|
334
|
+
// because other modules in this route's graph import their own bindings from
|
|
335
|
+
// it.
|
|
336
|
+
const reconcilePluginSourcesNowSpy = mock(() => Promise.resolve());
|
|
337
|
+
|
|
338
|
+
const actualMtimeCache: typeof import("../../../plugins/mtime-cache.js") =
|
|
339
|
+
await import("../../../plugins/mtime-cache.js");
|
|
340
|
+
|
|
341
|
+
mock.module("../../../plugins/mtime-cache.js", () => ({
|
|
342
|
+
...actualMtimeCache,
|
|
343
|
+
reconcilePluginSourcesNow: reconcilePluginSourcesNowSpy,
|
|
344
|
+
}));
|
|
345
|
+
|
|
328
346
|
// Spy on broadcastMessage so we can assert the sync_changed invalidation the
|
|
329
347
|
// enable/disable handlers emit. The handlers publish through the canonical
|
|
330
348
|
// `publishPluginsChanged` → `publishSyncInvalidation` path (both left real), so
|
|
@@ -2573,8 +2591,10 @@ function toggleResult(
|
|
|
2573
2591
|
};
|
|
2574
2592
|
}
|
|
2575
2593
|
|
|
2576
|
-
function invokeEnable(
|
|
2577
|
-
|
|
2594
|
+
async function invokeEnable(
|
|
2595
|
+
args: RouteHandlerArgs = {},
|
|
2596
|
+
): Promise<{ ok: boolean }> {
|
|
2597
|
+
return (await enableHandler(args)) as { ok: boolean };
|
|
2578
2598
|
}
|
|
2579
2599
|
|
|
2580
2600
|
function invokeDisable(args: RouteHandlerArgs = {}): { ok: boolean } {
|
|
@@ -2582,18 +2602,22 @@ function invokeDisable(args: RouteHandlerArgs = {}): { ok: boolean } {
|
|
|
2582
2602
|
}
|
|
2583
2603
|
|
|
2584
2604
|
/**
|
|
2585
|
-
* Wait for
|
|
2586
|
-
*
|
|
2587
|
-
* run on the handler's synchronous
|
|
2605
|
+
* Wait for a toggle handler's fire-and-forget reconcile poke to land. The poke
|
|
2606
|
+
* runs off the microtask queue (the disable path also resolves a lazy
|
|
2607
|
+
* `import()` first), so the assertion cannot run on the handler's synchronous
|
|
2608
|
+
* return.
|
|
2588
2609
|
*/
|
|
2589
|
-
async function
|
|
2610
|
+
async function waitForReconcile(
|
|
2611
|
+
spy: { mock: { calls: unknown[] } },
|
|
2612
|
+
label: string,
|
|
2613
|
+
): Promise<void> {
|
|
2590
2614
|
for (let i = 0; i < 50; i++) {
|
|
2591
|
-
if (
|
|
2615
|
+
if (spy.mock.calls.length > 0) {
|
|
2592
2616
|
return;
|
|
2593
2617
|
}
|
|
2594
2618
|
await new Promise((resolve) => setTimeout(resolve, 2));
|
|
2595
2619
|
}
|
|
2596
|
-
throw new Error(
|
|
2620
|
+
throw new Error(`${label} was never triggered`);
|
|
2597
2621
|
}
|
|
2598
2622
|
|
|
2599
2623
|
/** Assert the spy received exactly one sync_changed carrying `plugins:list`. */
|
|
@@ -2609,30 +2633,50 @@ describe("POST /v1/plugins/:name/enable", () => {
|
|
|
2609
2633
|
enablePluginSpy.mockReset();
|
|
2610
2634
|
broadcastMessageSpy.mockReset();
|
|
2611
2635
|
reconcilePluginSchedulesSpy.mockClear();
|
|
2636
|
+
reconcilePluginSourcesNowSpy.mockClear();
|
|
2612
2637
|
});
|
|
2613
2638
|
|
|
2614
|
-
test("
|
|
2639
|
+
test("awaits the source reconcile so a boot-disabled plugin activates before the route returns", async () => {
|
|
2615
2640
|
enablePluginSpy.mockImplementation((name) => toggleResult(name, "enable"));
|
|
2616
2641
|
|
|
2617
|
-
invokeEnable({ pathParams: { name: "simple-memory" } });
|
|
2642
|
+
await invokeEnable({ pathParams: { name: "simple-memory" } });
|
|
2643
|
+
|
|
2644
|
+
// The boot scan skipped this plugin for its sentinel, so clearing the
|
|
2645
|
+
// sentinel only brings its hooks, tools, MCP servers and schedules up if
|
|
2646
|
+
// the source reconcile runs. The route awaits it, so by the time the
|
|
2647
|
+
// response lands the reconcile has completed; it converges the schedules
|
|
2648
|
+
// itself, so the route does not poke them separately.
|
|
2649
|
+
expect(reconcilePluginSourcesNowSpy).toHaveBeenCalledTimes(1);
|
|
2650
|
+
expect(reconcilePluginSchedulesSpy).not.toHaveBeenCalled();
|
|
2651
|
+
});
|
|
2652
|
+
|
|
2653
|
+
test("a failed toggle does not run the source reconcile", async () => {
|
|
2654
|
+
enablePluginSpy.mockImplementation((name) => {
|
|
2655
|
+
throw new PluginDirectoryNotFoundError(name);
|
|
2656
|
+
});
|
|
2618
2657
|
|
|
2619
|
-
await
|
|
2658
|
+
await expect(
|
|
2659
|
+
invokeEnable({ pathParams: { name: "ghost" } }),
|
|
2660
|
+
).rejects.toThrow(NotFoundError);
|
|
2661
|
+
expect(reconcilePluginSourcesNowSpy).not.toHaveBeenCalled();
|
|
2620
2662
|
});
|
|
2621
2663
|
|
|
2622
|
-
test("enables the plugin and broadcasts sync_changed(plugins:list)", () => {
|
|
2664
|
+
test("enables the plugin and broadcasts sync_changed(plugins:list)", async () => {
|
|
2623
2665
|
enablePluginSpy.mockImplementation((name) => toggleResult(name, "enable"));
|
|
2624
2666
|
|
|
2625
|
-
const result = invokeEnable({
|
|
2667
|
+
const result = await invokeEnable({
|
|
2668
|
+
pathParams: { name: "simple-memory" },
|
|
2669
|
+
});
|
|
2626
2670
|
|
|
2627
2671
|
expect(result).toEqual({ ok: true });
|
|
2628
2672
|
expect(enablePluginSpy.mock.calls[0]?.[0]).toBe("simple-memory");
|
|
2629
2673
|
expectPluginsListBroadcast();
|
|
2630
2674
|
});
|
|
2631
2675
|
|
|
2632
|
-
test("threads x-vellum-client-id into the published event's originClientId", () => {
|
|
2676
|
+
test("threads x-vellum-client-id into the published event's originClientId", async () => {
|
|
2633
2677
|
enablePluginSpy.mockImplementation((name) => toggleResult(name, "enable"));
|
|
2634
2678
|
|
|
2635
|
-
invokeEnable({
|
|
2679
|
+
await invokeEnable({
|
|
2636
2680
|
pathParams: { name: "simple-memory" },
|
|
2637
2681
|
headers: { "x-vellum-client-id": "client-abc" },
|
|
2638
2682
|
});
|
|
@@ -2646,7 +2690,7 @@ describe("POST /v1/plugins/:name/enable", () => {
|
|
|
2646
2690
|
});
|
|
2647
2691
|
});
|
|
2648
2692
|
|
|
2649
|
-
test("a broadcast failure does not fail a successful toggle", () => {
|
|
2693
|
+
test("a broadcast failure does not fail a successful toggle", async () => {
|
|
2650
2694
|
enablePluginSpy.mockImplementation((name) => toggleResult(name, "enable"));
|
|
2651
2695
|
// The sentinel was already flipped; a hub throw AFTER that must not surface
|
|
2652
2696
|
// as a 500 — the canonical publisher swallows broadcast errors.
|
|
@@ -2654,40 +2698,42 @@ describe("POST /v1/plugins/:name/enable", () => {
|
|
|
2654
2698
|
throw new Error("hub unavailable");
|
|
2655
2699
|
});
|
|
2656
2700
|
|
|
2657
|
-
const result = invokeEnable({
|
|
2701
|
+
const result = await invokeEnable({
|
|
2702
|
+
pathParams: { name: "simple-memory" },
|
|
2703
|
+
});
|
|
2658
2704
|
expect(result).toEqual({ ok: true });
|
|
2659
2705
|
});
|
|
2660
2706
|
|
|
2661
|
-
test("PluginAlreadyInStateException → ConflictError (409), no broadcast", () => {
|
|
2707
|
+
test("PluginAlreadyInStateException → ConflictError (409), no broadcast", async () => {
|
|
2662
2708
|
enablePluginSpy.mockImplementation((name) => {
|
|
2663
2709
|
throw new PluginAlreadyInStateException(name, "enable");
|
|
2664
2710
|
});
|
|
2665
2711
|
|
|
2666
|
-
expect(
|
|
2712
|
+
await expect(
|
|
2667
2713
|
invokeEnable({ pathParams: { name: "simple-memory" } }),
|
|
2668
|
-
).toThrow(ConflictError);
|
|
2714
|
+
).rejects.toThrow(ConflictError);
|
|
2669
2715
|
// A no-op toggle must not fan out a spurious invalidation.
|
|
2670
2716
|
expect(broadcastMessageSpy).not.toHaveBeenCalled();
|
|
2671
2717
|
});
|
|
2672
2718
|
|
|
2673
|
-
test("PluginDirectoryNotFoundError → NotFoundError (404)", () => {
|
|
2719
|
+
test("PluginDirectoryNotFoundError → NotFoundError (404)", async () => {
|
|
2674
2720
|
enablePluginSpy.mockImplementation((name) => {
|
|
2675
2721
|
throw new PluginDirectoryNotFoundError(name);
|
|
2676
2722
|
});
|
|
2677
2723
|
|
|
2678
|
-
expect(
|
|
2679
|
-
|
|
2680
|
-
);
|
|
2724
|
+
await expect(
|
|
2725
|
+
invokeEnable({ pathParams: { name: "ghost" } }),
|
|
2726
|
+
).rejects.toThrow(NotFoundError);
|
|
2681
2727
|
});
|
|
2682
2728
|
|
|
2683
|
-
test("InvalidPluginNameError → BadRequestError (400)", () => {
|
|
2729
|
+
test("InvalidPluginNameError → BadRequestError (400)", async () => {
|
|
2684
2730
|
enablePluginSpy.mockImplementation(() => {
|
|
2685
2731
|
throw new ToggleInvalidPluginNameError("../escape");
|
|
2686
2732
|
});
|
|
2687
2733
|
|
|
2688
|
-
expect(
|
|
2689
|
-
|
|
2690
|
-
);
|
|
2734
|
+
await expect(
|
|
2735
|
+
invokeEnable({ pathParams: { name: "../escape" } }),
|
|
2736
|
+
).rejects.toThrow(BadRequestError);
|
|
2691
2737
|
});
|
|
2692
2738
|
});
|
|
2693
2739
|
|
|
@@ -2696,6 +2742,7 @@ describe("POST /v1/plugins/:name/disable", () => {
|
|
|
2696
2742
|
disablePluginSpy.mockReset();
|
|
2697
2743
|
broadcastMessageSpy.mockReset();
|
|
2698
2744
|
reconcilePluginSchedulesSpy.mockClear();
|
|
2745
|
+
reconcilePluginSourcesNowSpy.mockClear();
|
|
2699
2746
|
});
|
|
2700
2747
|
|
|
2701
2748
|
test("reconciles plugin-declared schedules so the plugin's rows disarm now", async () => {
|
|
@@ -2706,8 +2753,14 @@ describe("POST /v1/plugins/:name/disable", () => {
|
|
|
2706
2753
|
invokeDisable({ pathParams: { name: "simple-memory" } });
|
|
2707
2754
|
|
|
2708
2755
|
// Without this poke the rows stay armed until the reconciler's next
|
|
2709
|
-
// backstop sweep.
|
|
2710
|
-
|
|
2756
|
+
// backstop sweep. Disable stays on the schedule poke: the sentinel filters
|
|
2757
|
+
// the plugin out at read time, and a source reconcile here would run its
|
|
2758
|
+
// shutdown hooks.
|
|
2759
|
+
await waitForReconcile(
|
|
2760
|
+
reconcilePluginSchedulesSpy,
|
|
2761
|
+
"plugin schedule reconcile",
|
|
2762
|
+
);
|
|
2763
|
+
expect(reconcilePluginSourcesNowSpy).not.toHaveBeenCalled();
|
|
2711
2764
|
});
|
|
2712
2765
|
|
|
2713
2766
|
test("a failed toggle does not poke the schedule reconcile", async () => {
|
|
@@ -26,6 +26,10 @@ interface StubConversation {
|
|
|
26
26
|
handleSurfaceUndoCalled?: boolean;
|
|
27
27
|
handleSurfaceUndoThrows?: Error;
|
|
28
28
|
trustContext?: { trustClass: string; sourceChannel: string };
|
|
29
|
+
/** Drives `isProcessing()`; the route must not stamp trust either way. */
|
|
30
|
+
processing?: boolean;
|
|
31
|
+
/** Trust the route threaded into `handleSurfaceAction`. */
|
|
32
|
+
lastRequesterTrustContext?: { trustClass: string; sourceChannel: string };
|
|
29
33
|
surfaceActionCalls: Array<{
|
|
30
34
|
surfaceId: string;
|
|
31
35
|
actionId: string;
|
|
@@ -160,7 +164,9 @@ function makeStub(id: string): StubConversation {
|
|
|
160
164
|
actionId: string,
|
|
161
165
|
data: unknown,
|
|
162
166
|
sourceActorPrincipalId?: string,
|
|
167
|
+
requesterTrustContext?: { trustClass: string; sourceChannel: string },
|
|
163
168
|
) => {
|
|
169
|
+
stub.lastRequesterTrustContext = requesterTrustContext;
|
|
164
170
|
stub.surfaceActionCalls.push({
|
|
165
171
|
surfaceId,
|
|
166
172
|
actionId,
|
|
@@ -181,6 +187,7 @@ function makeStub(id: string): StubConversation {
|
|
|
181
187
|
setTrustContext: (ctx: { trustClass: string; sourceChannel: string }) => {
|
|
182
188
|
stub.trustContext = ctx;
|
|
183
189
|
},
|
|
190
|
+
isProcessing: () => stub.processing === true,
|
|
184
191
|
});
|
|
185
192
|
return stub;
|
|
186
193
|
}
|
|
@@ -487,8 +494,8 @@ describe("triggerSurfaceAction trust context", () => {
|
|
|
487
494
|
headers: { "x-vellum-actor-principal-id": GUARDIAN_PRINCIPAL },
|
|
488
495
|
});
|
|
489
496
|
|
|
490
|
-
expect(live.
|
|
491
|
-
expect(live.
|
|
497
|
+
expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
|
|
498
|
+
expect(live.lastRequesterTrustContext?.sourceChannel).toBe("vellum");
|
|
492
499
|
// First-pass resolve already granted guardian, so the drift helper is skipped.
|
|
493
500
|
expect(reResolveCalls).toEqual([]);
|
|
494
501
|
});
|
|
@@ -506,7 +513,7 @@ describe("triggerSurfaceAction trust context", () => {
|
|
|
506
513
|
});
|
|
507
514
|
|
|
508
515
|
expect(reResolveCalls).toEqual([VELLUM_PRINCIPAL_OLD]);
|
|
509
|
-
expect(live.
|
|
516
|
+
expect(live.lastRequesterTrustContext?.trustClass).toBe("unknown");
|
|
510
517
|
});
|
|
511
518
|
|
|
512
519
|
test("reset drift: helper returns guardian → route adopts it", async () => {
|
|
@@ -522,7 +529,7 @@ describe("triggerSurfaceAction trust context", () => {
|
|
|
522
529
|
});
|
|
523
530
|
|
|
524
531
|
expect(reResolveCalls).toEqual([VELLUM_PRINCIPAL_OLD]);
|
|
525
|
-
expect(live.
|
|
532
|
+
expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
|
|
526
533
|
});
|
|
527
534
|
|
|
528
535
|
test("dev-bypass resolves the real guardian principal from the gateway, helper not called", async () => {
|
|
@@ -539,7 +546,7 @@ describe("triggerSurfaceAction trust context", () => {
|
|
|
539
546
|
|
|
540
547
|
// The synthetic dev-bypass principal is translated to the real guardian,
|
|
541
548
|
// yielding a guardian trust context without consulting the drift helper.
|
|
542
|
-
expect(live.
|
|
549
|
+
expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
|
|
543
550
|
expect(reResolveCalls).toEqual([]);
|
|
544
551
|
});
|
|
545
552
|
|
|
@@ -559,7 +566,49 @@ describe("triggerSurfaceAction trust context", () => {
|
|
|
559
566
|
headers: { "x-vellum-actor-principal-id": "dev-bypass" },
|
|
560
567
|
});
|
|
561
568
|
|
|
562
|
-
expect(live.
|
|
569
|
+
expect(live.lastRequesterTrustContext?.trustClass).toBe("unknown");
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("idle conversation: the requester is threaded to the action and the slot is left to the dispatch", async () => {
|
|
573
|
+
mockGuardianList = [guardianDelivery(GUARDIAN_PRINCIPAL)];
|
|
574
|
+
const live = makeStub("conv-idle");
|
|
575
|
+
live.processing = false;
|
|
576
|
+
memoryBySurface = live;
|
|
577
|
+
|
|
578
|
+
const handler = findHandler("triggerSurfaceAction");
|
|
579
|
+
await handler({
|
|
580
|
+
body: { surfaceId: "surf-idle", actionId: "act-idle" },
|
|
581
|
+
headers: { "x-vellum-actor-principal-id": GUARDIAN_PRINCIPAL },
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
|
|
585
|
+
// The route resolves; `handleSurfaceAction` stamps, and only where it
|
|
586
|
+
// commits to running the turn. Stamping here would repoint the actor of a
|
|
587
|
+
// turn that starts between this call and the dispatch.
|
|
588
|
+
expect(live.trustContext).toBeUndefined();
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
test("busy conversation: the running turn's actor is left alone and the click still travels as its own actor", async () => {
|
|
592
|
+
mockGuardianList = [guardianDelivery(GUARDIAN_PRINCIPAL)];
|
|
593
|
+
const live = makeStub("conv-busy");
|
|
594
|
+
// A turn is already running under a different actor.
|
|
595
|
+
const runningTurnActor = {
|
|
596
|
+
trustClass: "untrusted",
|
|
597
|
+
sourceChannel: "sms",
|
|
598
|
+
};
|
|
599
|
+
live.trustContext = runningTurnActor;
|
|
600
|
+
live.processing = true;
|
|
601
|
+
memoryBySurface = live;
|
|
602
|
+
|
|
603
|
+
const handler = findHandler("triggerSurfaceAction");
|
|
604
|
+
await handler({
|
|
605
|
+
body: { surfaceId: "surf-busy", actionId: "act-busy" },
|
|
606
|
+
headers: { "x-vellum-actor-principal-id": GUARDIAN_PRINCIPAL },
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
// Identical object, so a re-write of an equal value still fails this.
|
|
610
|
+
expect(live.trustContext).toBe(runningTurnActor);
|
|
611
|
+
expect(live.lastRequesterTrustContext?.trustClass).toBe("guardian");
|
|
563
612
|
});
|
|
564
613
|
});
|
|
565
614
|
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests which system cards terminate a turn.
|
|
3
|
+
*
|
|
4
|
+
* Clients treat `message_complete` as terminal: it clears the processing state
|
|
5
|
+
* and closes the turn. A card that is itself the reply (a slash-command
|
|
6
|
+
* result) must emit it; a card a plugin posts while a turn is still running
|
|
7
|
+
* must not, or the client ends a turn that is still streaming.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { setConfig } from "../../../__tests__/helpers/set-config.js";
|
|
13
|
+
import type { AssistantEvent } from "../../../api/index.js";
|
|
14
|
+
|
|
15
|
+
// Keep the memory system off so addMessage skips indexing side effects.
|
|
16
|
+
setConfig("memory", { enabled: false });
|
|
17
|
+
|
|
18
|
+
// Capture what reaches connected clients, keeping the rest of the hub's
|
|
19
|
+
// surface intact for the module graph's other importers.
|
|
20
|
+
const eventHub = await import("../../assistant-event-hub.js");
|
|
21
|
+
const broadcastEvents: AssistantEvent[] = [];
|
|
22
|
+
|
|
23
|
+
/** The terminal turn events among everything broadcast so far. */
|
|
24
|
+
function completions(): AssistantEvent[] {
|
|
25
|
+
return broadcastEvents.filter((e) => e.type === "message_complete");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
mock.module("../../assistant-event-hub.js", () => ({
|
|
29
|
+
...eventHub,
|
|
30
|
+
broadcastMessage: (event: AssistantEvent) => {
|
|
31
|
+
broadcastEvents.push(event);
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const { createConversation } =
|
|
36
|
+
await import("../../../persistence/conversation-crud.js");
|
|
37
|
+
const { initializeDb } = await import("../../../persistence/db-init.js");
|
|
38
|
+
const { persistSystemCard } = await import("../canned-message-complete.js");
|
|
39
|
+
|
|
40
|
+
await initializeDb();
|
|
41
|
+
|
|
42
|
+
describe("persistSystemCard turn termination", () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
broadcastEvents.length = 0;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("announces a card that is the reply as a completed turn", async () => {
|
|
48
|
+
/**
|
|
49
|
+
* Tests that a card standing in for the assistant's reply closes the turn
|
|
50
|
+
* so the client stops showing it as processing.
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
// GIVEN a conversation
|
|
54
|
+
const conv = createConversation();
|
|
55
|
+
|
|
56
|
+
// WHEN a card that ends the turn is persisted
|
|
57
|
+
const card = await persistSystemCard({
|
|
58
|
+
conversationId: conv.id,
|
|
59
|
+
text: "Compacted 12 messages.",
|
|
60
|
+
metadata: {},
|
|
61
|
+
endsTurn: true,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// THEN clients are told the turn completed, pointing at the card row
|
|
65
|
+
expect(completions()).toEqual([
|
|
66
|
+
{
|
|
67
|
+
type: "message_complete",
|
|
68
|
+
conversationId: conv.id,
|
|
69
|
+
messageId: card.id,
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("leaves an in-flight turn running when a card is posted mid-turn", async () => {
|
|
75
|
+
/**
|
|
76
|
+
* Tests that a card written while the model is still working does not end
|
|
77
|
+
* the turn in the client, which would clear processing state and let a
|
|
78
|
+
* second message start while the first turn is still streaming.
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
// GIVEN a conversation
|
|
82
|
+
const conv = createConversation();
|
|
83
|
+
|
|
84
|
+
// WHEN a non-terminal card is persisted
|
|
85
|
+
const card = await persistSystemCard({
|
|
86
|
+
conversationId: conv.id,
|
|
87
|
+
text: "The image you attached was not sent to the model.",
|
|
88
|
+
metadata: { plugin: "image-fallback" },
|
|
89
|
+
endsTurn: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// THEN the card is persisted
|
|
93
|
+
expect(card.id).toBeTruthy();
|
|
94
|
+
|
|
95
|
+
// AND no terminal turn event reaches clients
|
|
96
|
+
expect(completions()).toEqual([]);
|
|
97
|
+
|
|
98
|
+
// AND clients are still told to refetch the transcript so the card shows
|
|
99
|
+
expect(
|
|
100
|
+
broadcastEvents.some(
|
|
101
|
+
(e) =>
|
|
102
|
+
e.type === "sync_changed" &&
|
|
103
|
+
e.tags.includes(`conversation:${conv.id}:messages`),
|
|
104
|
+
),
|
|
105
|
+
).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|