@vellumai/assistant 0.11.4 → 0.11.5-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/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__/config-loader-backfill.test.ts +19 -10
- 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__/managed-profile-guard.test.ts +4 -2
- 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/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- 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/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- 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/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, rmSync } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
4
|
+
import { afterAll, afterEach, describe, expect, test } from "bun:test";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
ensureDataDir,
|
|
@@ -23,6 +23,21 @@ import {
|
|
|
23
23
|
} from "../util/platform.js";
|
|
24
24
|
|
|
25
25
|
const originalWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
26
|
+
|
|
27
|
+
// This file characterizes path resolution itself (including the ~/.vellum
|
|
28
|
+
// fallback and literal override paths) without reading or writing those
|
|
29
|
+
// locations, so the live-workspace guard is deliberately bypassed.
|
|
30
|
+
const originalAllowRealWorkspace =
|
|
31
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
32
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS = "1";
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
if (originalAllowRealWorkspace === undefined) {
|
|
35
|
+
delete process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS;
|
|
36
|
+
} else {
|
|
37
|
+
process.env.VELLUM_ALLOW_REAL_WORKSPACE_IN_TESTS =
|
|
38
|
+
originalAllowRealWorkspace;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
26
41
|
const originalVellumEnvironment = process.env.VELLUM_ENVIRONMENT;
|
|
27
42
|
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
28
43
|
|
|
@@ -102,21 +102,28 @@ describe("resolveCredential", () => {
|
|
|
102
102
|
});
|
|
103
103
|
|
|
104
104
|
describe("plugin scoping", () => {
|
|
105
|
-
test("allows a plugin to resolve a credential whose
|
|
106
|
-
seedCredential("
|
|
107
|
-
const value = await runInPluginContext("
|
|
108
|
-
resolveCredential("
|
|
105
|
+
test("allows a plugin to resolve a credential whose service matches its name", async () => {
|
|
106
|
+
seedCredential("imessage", "api_key", "comms-key");
|
|
107
|
+
const value = await runInPluginContext("imessage", () =>
|
|
108
|
+
resolveCredential("imessage/api_key"),
|
|
109
109
|
);
|
|
110
|
-
expect(value).toBe("
|
|
110
|
+
expect(value).toBe("comms-key");
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
-
test("blocks a plugin from resolving
|
|
113
|
+
test("blocks a plugin from resolving another service", async () => {
|
|
114
114
|
seedCredential("openai", "api_key", "sk-secret");
|
|
115
115
|
await expect(
|
|
116
116
|
runInPluginContext("acme", () => resolveCredential("openai/api_key")),
|
|
117
117
|
).rejects.toThrow(/out of scope/);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
test("blocks a plugin from resolving another service even when the field matches its name", async () => {
|
|
121
|
+
seedCredential("openai", "acme", "scoped-secret");
|
|
122
|
+
await expect(
|
|
123
|
+
runInPluginContext("acme", () => resolveCredential("openai/acme")),
|
|
124
|
+
).rejects.toThrow(/out of scope/);
|
|
125
|
+
});
|
|
126
|
+
|
|
120
127
|
test("does not read the secure backend when out of scope", async () => {
|
|
121
128
|
seedCredential("openai", "api_key", "sk-secret");
|
|
122
129
|
getSpy.mockClear();
|
|
@@ -126,15 +133,17 @@ describe("resolveCredential", () => {
|
|
|
126
133
|
expect(getSpy).not.toHaveBeenCalled();
|
|
127
134
|
});
|
|
128
135
|
|
|
129
|
-
test("scoping applies
|
|
130
|
-
seedCredential("
|
|
131
|
-
seedCredential("
|
|
136
|
+
test("scoping applies to every field under the plugin's service", async () => {
|
|
137
|
+
seedCredential("imessage", "api_key", "comms-key");
|
|
138
|
+
seedCredential("imessage", "photon_project_id", "proj-id");
|
|
132
139
|
await expect(
|
|
133
|
-
runInPluginContext("
|
|
134
|
-
).resolves.toBe("
|
|
140
|
+
runInPluginContext("imessage", () => resolveCredential("imessage/api_key")),
|
|
141
|
+
).resolves.toBe("comms-key");
|
|
135
142
|
await expect(
|
|
136
|
-
runInPluginContext("
|
|
137
|
-
|
|
143
|
+
runInPluginContext("imessage", () =>
|
|
144
|
+
resolveCredential("imessage/photon_project_id"),
|
|
145
|
+
),
|
|
146
|
+
).resolves.toBe("proj-id");
|
|
138
147
|
});
|
|
139
148
|
});
|
|
140
149
|
});
|
|
@@ -136,9 +136,14 @@ describe("ensurePluginApiShim", () => {
|
|
|
136
136
|
`}));\n`,
|
|
137
137
|
);
|
|
138
138
|
|
|
139
|
+
// Explicit env spread: under bun test, a child's default env reflects the
|
|
140
|
+
// process-start environ, not later process.env mutations. Without this
|
|
141
|
+
// the child misses the preload's VELLUM_WORKSPACE_DIR and resolves the
|
|
142
|
+
// real ~/.vellum/workspace (refused by the live-workspace guard).
|
|
139
143
|
const result = spawnSync(process.execPath, ["run", probe], {
|
|
140
144
|
encoding: "utf8",
|
|
141
145
|
timeout: 30_000,
|
|
146
|
+
env: { ...process.env },
|
|
142
147
|
});
|
|
143
148
|
expect(result.status).toBe(0);
|
|
144
149
|
const lastLine = result.stdout.trim().split("\n").at(-1) ?? "";
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
afterAll,
|
|
7
|
+
afterEach,
|
|
8
|
+
beforeEach,
|
|
9
|
+
describe,
|
|
10
|
+
expect,
|
|
11
|
+
spyOn,
|
|
12
|
+
test,
|
|
13
|
+
} from "bun:test";
|
|
14
|
+
|
|
15
|
+
import { credentialKey } from "@vellumai/credential-storage";
|
|
16
|
+
|
|
17
|
+
import * as scrub from "../daemon/credential-transcript-scrub.js";
|
|
18
|
+
import * as manualToken from "../oauth/manual-token-connection.js";
|
|
19
|
+
import { resolveCredential } from "../plugin-api/resolve-credential.js";
|
|
20
|
+
import {
|
|
21
|
+
CredentialStoreError,
|
|
22
|
+
storeCredential,
|
|
23
|
+
} from "../plugin-api/store-credential.js";
|
|
24
|
+
import { runInPluginContext } from "../plugins/plugin-execution-context.js";
|
|
25
|
+
import * as secureKeys from "../security/secure-keys.js";
|
|
26
|
+
import {
|
|
27
|
+
_setMetadataPath,
|
|
28
|
+
getCredentialMetadata,
|
|
29
|
+
} from "../tools/credentials/metadata-store.js";
|
|
30
|
+
import { storeCredentialValue } from "../tools/credentials/store.js";
|
|
31
|
+
|
|
32
|
+
// Real metadata store backed by a temp file. A mock.module on metadata-store
|
|
33
|
+
// or secure-keys would replace the whole module namespace, so only the secure
|
|
34
|
+
// backend access, the transcript scrub, and the manual-token connection sync
|
|
35
|
+
// are intercepted, via restorable spies.
|
|
36
|
+
|
|
37
|
+
const TEST_DIR = join(
|
|
38
|
+
tmpdir(),
|
|
39
|
+
`vellum-plugin-storecred-${randomBytes(4).toString("hex")}`,
|
|
40
|
+
);
|
|
41
|
+
const META_PATH = join(TEST_DIR, "metadata.json");
|
|
42
|
+
|
|
43
|
+
/** The plugin every scoped case runs as: it owns the `acme` service. */
|
|
44
|
+
const PLUGIN = "acme";
|
|
45
|
+
|
|
46
|
+
/** Run `fn` as the plugin would: inside its execution context. */
|
|
47
|
+
function asPlugin<T>(fn: () => T): T {
|
|
48
|
+
return runInPluginContext(PLUGIN, fn);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let secureStore: Map<string, string>;
|
|
52
|
+
let writeSucceeds: boolean;
|
|
53
|
+
let setSpy: ReturnType<typeof spyOn>;
|
|
54
|
+
let getSpy: ReturnType<typeof spyOn>;
|
|
55
|
+
let scrubSpy: ReturnType<typeof spyOn>;
|
|
56
|
+
let syncSpy: ReturnType<typeof spyOn>;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
if (existsSync(TEST_DIR)) {
|
|
60
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
63
|
+
_setMetadataPath(META_PATH);
|
|
64
|
+
|
|
65
|
+
secureStore = new Map();
|
|
66
|
+
writeSucceeds = true;
|
|
67
|
+
setSpy = spyOn(secureKeys, "setSecureKeyAsync").mockImplementation(
|
|
68
|
+
async (key: string, value: string) => {
|
|
69
|
+
if (!writeSucceeds) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
secureStore.set(key, value);
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
getSpy = spyOn(secureKeys, "getSecureKeyResultAsync").mockImplementation(
|
|
77
|
+
async (key: string) => ({
|
|
78
|
+
value: secureStore.get(key),
|
|
79
|
+
unreachable: false,
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
scrubSpy = spyOn(
|
|
83
|
+
scrub,
|
|
84
|
+
"scrubStoredCredentialFromTranscripts",
|
|
85
|
+
).mockImplementation(async () => ({
|
|
86
|
+
dbMessagesScrubbed: 0,
|
|
87
|
+
residentMessagesScrubbed: 0,
|
|
88
|
+
}));
|
|
89
|
+
syncSpy = spyOn(manualToken, "syncManualTokenConnection").mockImplementation(
|
|
90
|
+
async () => {},
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
afterEach(() => {
|
|
95
|
+
setSpy.mockRestore();
|
|
96
|
+
getSpy.mockRestore();
|
|
97
|
+
scrubSpy.mockRestore();
|
|
98
|
+
syncSpy.mockRestore();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
afterAll(() => {
|
|
102
|
+
_setMetadataPath(null);
|
|
103
|
+
if (existsSync(TEST_DIR)) {
|
|
104
|
+
rmSync(TEST_DIR, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("storeCredential", () => {
|
|
109
|
+
test("creates a credential from a service/field ref", async () => {
|
|
110
|
+
const stored = await asPlugin(() =>
|
|
111
|
+
storeCredential("acme/api_key", "sk-secret"),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(stored.service).toBe("acme");
|
|
115
|
+
expect(stored.field).toBe("api_key");
|
|
116
|
+
expect(stored.credentialId).toBeTruthy();
|
|
117
|
+
expect(secureStore.get(credentialKey("acme", "api_key"))).toBe("sk-secret");
|
|
118
|
+
expect(getCredentialMetadata("acme", "api_key")?.credentialId).toBe(
|
|
119
|
+
stored.credentialId,
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("stores a value that resolveCredential reads back", async () => {
|
|
124
|
+
await asPlugin(() => storeCredential("acme/api_key", "sk-secret"));
|
|
125
|
+
await expect(
|
|
126
|
+
asPlugin(() => resolveCredential("acme/api_key")),
|
|
127
|
+
).resolves.toBe("sk-secret");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("trims edge whitespace from the value", async () => {
|
|
131
|
+
await asPlugin(() => storeCredential("acme/api_key", " sk-secret\n"));
|
|
132
|
+
expect(secureStore.get(credentialKey("acme", "api_key"))).toBe("sk-secret");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("records label and description on the credential metadata", async () => {
|
|
136
|
+
await asPlugin(() =>
|
|
137
|
+
storeCredential("acme/api_key", "sk-secret", {
|
|
138
|
+
label: "Acme key",
|
|
139
|
+
description: "Used by the acme plugin",
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const metadata = getCredentialMetadata("acme", "api_key");
|
|
144
|
+
expect(metadata?.alias).toBe("Acme key");
|
|
145
|
+
expect(metadata?.usageDescription).toBe("Used by the acme plugin");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("replaces the value of an existing credential named by UUID", async () => {
|
|
149
|
+
const first = await asPlugin(() =>
|
|
150
|
+
storeCredential("acme/api_key", "sk-old"),
|
|
151
|
+
);
|
|
152
|
+
const second = await asPlugin(() =>
|
|
153
|
+
storeCredential(first.credentialId, "sk-new"),
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
expect(second).toEqual(first);
|
|
157
|
+
expect(secureStore.get(credentialKey("acme", "api_key"))).toBe("sk-new");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("rejects a ref that names no credential and is not service/field", async () => {
|
|
161
|
+
await expect(
|
|
162
|
+
asPlugin(() => storeCredential("openai", "sk-secret")),
|
|
163
|
+
).rejects.toBeInstanceOf(CredentialStoreError);
|
|
164
|
+
expect(setSpy).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("rejects an empty value", async () => {
|
|
168
|
+
await expect(
|
|
169
|
+
asPlugin(() => storeCredential("acme/api_key", " ")),
|
|
170
|
+
).rejects.toThrow(/value is required/);
|
|
171
|
+
expect(setSpy).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("surfaces a failed secure-backend write", async () => {
|
|
175
|
+
writeSucceeds = false;
|
|
176
|
+
await expect(
|
|
177
|
+
asPlugin(() => storeCredential("acme/api_key", "sk-secret")),
|
|
178
|
+
).rejects.toThrow(CredentialStoreError);
|
|
179
|
+
expect(getCredentialMetadata("acme", "api_key")).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("scrubs the stored value from recent transcripts", async () => {
|
|
183
|
+
await asPlugin(() => storeCredential("acme/api_key", "sk-secret"));
|
|
184
|
+
expect(scrubSpy).toHaveBeenCalledWith("sk-secret");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("skips the transcript scrub when asked", async () => {
|
|
188
|
+
await asPlugin(() =>
|
|
189
|
+
storeCredential("acme/api_key", "sk-secret", {
|
|
190
|
+
skipTranscriptScrub: true,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
expect(scrubSpy).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("reconciles the manual-token connection for the service", async () => {
|
|
197
|
+
await asPlugin(() => storeCredential("acme/api_key", "sk-secret"));
|
|
198
|
+
expect(syncSpy).toHaveBeenCalledWith("acme");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("plugin scoping", () => {
|
|
202
|
+
test("fails closed with no plugin in context", async () => {
|
|
203
|
+
// A plugin's module body is evaluated outside any context. An unscoped
|
|
204
|
+
// branch there would let top-level code overwrite the user's own
|
|
205
|
+
// credentials, so the write is refused instead.
|
|
206
|
+
await expect(storeCredential("acme/api_key", "sk-secret")).rejects.toThrow(
|
|
207
|
+
/requires an active plugin execution context/,
|
|
208
|
+
);
|
|
209
|
+
expect(setSpy).not.toHaveBeenCalled();
|
|
210
|
+
expect(getCredentialMetadata("acme", "api_key")).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("blocks a plugin from storing a credential it does not own", async () => {
|
|
214
|
+
await expect(
|
|
215
|
+
asPlugin(() => storeCredential("openai/api_key", "sk-secret")),
|
|
216
|
+
).rejects.toThrow(/out of scope/);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("does not write the secure backend when out of scope", async () => {
|
|
220
|
+
await expect(
|
|
221
|
+
asPlugin(() => storeCredential("openai/api_key", "sk-secret")),
|
|
222
|
+
).rejects.toThrow(CredentialStoreError);
|
|
223
|
+
expect(setSpy).not.toHaveBeenCalled();
|
|
224
|
+
expect(getCredentialMetadata("openai", "api_key")).toBeUndefined();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("blocks a plugin from overwriting another credential by UUID", async () => {
|
|
228
|
+
// Seeded through the host write path, the way the user's own credential
|
|
229
|
+
// is created (`credentials set`), not through the plugin API.
|
|
230
|
+
const existing = await storeCredentialValue({
|
|
231
|
+
service: "openai",
|
|
232
|
+
field: "api_key",
|
|
233
|
+
value: "sk-user",
|
|
234
|
+
skipTranscriptScrub: true,
|
|
235
|
+
});
|
|
236
|
+
setSpy.mockClear();
|
|
237
|
+
|
|
238
|
+
await expect(
|
|
239
|
+
asPlugin(() => storeCredential(existing.credentialId, "sk-hijacked")),
|
|
240
|
+
).rejects.toThrow(/out of scope/);
|
|
241
|
+
expect(setSpy).not.toHaveBeenCalled();
|
|
242
|
+
expect(secureStore.get(credentialKey("openai", "api_key"))).toBe(
|
|
243
|
+
"sk-user",
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("blocks a plugin from storing another service even when the field matches its name", async () => {
|
|
248
|
+
await expect(
|
|
249
|
+
asPlugin(() => storeCredential("openai/acme", "sk-secret")),
|
|
250
|
+
).rejects.toThrow(/out of scope/);
|
|
251
|
+
expect(setSpy).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("allows a plugin to store multiple fields under its own service", async () => {
|
|
255
|
+
await asPlugin(async () => {
|
|
256
|
+
await storeCredential("acme/api_key", "key-secret");
|
|
257
|
+
await storeCredential("acme/token", "token-secret");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
expect(secureStore.get(credentialKey("acme", "api_key"))).toBe(
|
|
261
|
+
"key-secret",
|
|
262
|
+
);
|
|
263
|
+
expect(secureStore.get(credentialKey("acme", "token"))).toBe(
|
|
264
|
+
"token-secret",
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -32,7 +32,10 @@ import {
|
|
|
32
32
|
populateCacheAtBoot,
|
|
33
33
|
resetPluginCacheForTests,
|
|
34
34
|
} from "../plugins/mtime-cache.js";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
PRESERVED_ENTRIES,
|
|
37
|
+
USER_STATE_ENTRIES,
|
|
38
|
+
} from "../plugins/plugin-tree-walk.js";
|
|
36
39
|
|
|
37
40
|
const ROOT = join(
|
|
38
41
|
tmpdir(),
|
|
@@ -87,6 +90,10 @@ describe("PRESERVED_ENTRIES", () => {
|
|
|
87
90
|
expect(PRESERVED_ENTRIES).toContain("data");
|
|
88
91
|
expect(PRESERVED_ENTRIES).toContain(".disabled");
|
|
89
92
|
});
|
|
93
|
+
|
|
94
|
+
test("user-state entries are the preserved set minus the provenance sidecar", () => {
|
|
95
|
+
expect([...USER_STATE_ENTRIES]).toEqual(["config.json", "data", ".disabled"]);
|
|
96
|
+
});
|
|
90
97
|
});
|
|
91
98
|
|
|
92
99
|
describe("fingerprint exclusion of preserved entries", () => {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
getRegisteredInjectors,
|
|
23
23
|
registerPluginInjectors,
|
|
24
24
|
} from "../plugins/injector-registry.js";
|
|
25
|
+
import { resetPluginCacheForTests } from "../plugins/mtime-cache.js";
|
|
25
26
|
import {
|
|
26
27
|
registerPlugin,
|
|
27
28
|
resetPluginRegistryForTests,
|
|
@@ -85,6 +86,7 @@ function makeFakeTool(name: string): Tool {
|
|
|
85
86
|
|
|
86
87
|
beforeEach(() => {
|
|
87
88
|
resetPluginRegistryForTests();
|
|
89
|
+
resetPluginCacheForTests();
|
|
88
90
|
clearInjectorRegistry();
|
|
89
91
|
});
|
|
90
92
|
|
|
@@ -8,14 +8,18 @@
|
|
|
8
8
|
* non-null Set is an allowlist, `null` means no restriction. This suite locks
|
|
9
9
|
* the contract that the hook gather sites honor it:
|
|
10
10
|
* - `getHooksFor(name, { conversationId })` resolves the conversation's scope
|
|
11
|
-
* and excludes
|
|
11
|
+
* and excludes user-plugin hooks outside it. First-party default plugins
|
|
12
|
+
* always remain (they are core runtime, not per-chat toggles). Omitting the
|
|
12
13
|
* conversationId imposes no restriction.
|
|
14
|
+
* - `getUserHooksFor` returns those same in-process default-plugin hooks
|
|
15
|
+
* (plus user-land hooks), not only filesystem plugins.
|
|
13
16
|
* - `collectUserHookEntries(name, dirs, set)` excludes user-land plugin hooks
|
|
14
17
|
* outside the set, while standalone workspace hooks (not owned by a plugin)
|
|
15
18
|
* still run.
|
|
16
19
|
*
|
|
17
20
|
* The per-chat scope layers on top of (does not replace) the global
|
|
18
|
-
* `.disabled` sentinel check
|
|
21
|
+
* `.disabled` sentinel check. A user plugin excluded by either is excluded.
|
|
22
|
+
* A first-party default plugin is excluded only by `.disabled`.
|
|
19
23
|
* Injectors are intentionally NOT per-chat scoped (see
|
|
20
24
|
* `getRegisteredInjectors`): they run inside already-scoped plugin hooks, and
|
|
21
25
|
* every injector-contributing plugin is a first-party default.
|
|
@@ -39,7 +43,8 @@ import {
|
|
|
39
43
|
collectUserHookEntries,
|
|
40
44
|
resetHookCacheForTests,
|
|
41
45
|
} from "../hooks/hook-loader.js";
|
|
42
|
-
import { getHooksFor } from "../hooks/registry.js";
|
|
46
|
+
import { getHooksFor, getUserHooksFor } from "../hooks/registry.js";
|
|
47
|
+
import { resetPluginCacheForTests } from "../plugins/mtime-cache.js";
|
|
43
48
|
import {
|
|
44
49
|
registerPlugin,
|
|
45
50
|
resetPluginRegistryForTests,
|
|
@@ -66,6 +71,7 @@ function buildPlugin(
|
|
|
66
71
|
describe("getHooksFor per-chat plugin scope (in-process default hooks)", () => {
|
|
67
72
|
beforeEach(() => {
|
|
68
73
|
resetPluginRegistryForTests();
|
|
74
|
+
resetPluginCacheForTests();
|
|
69
75
|
resolveScopeMock.mockReset();
|
|
70
76
|
resolveScopeMock.mockImplementation(() => null);
|
|
71
77
|
});
|
|
@@ -136,6 +142,52 @@ describe("getHooksFor per-chat plugin scope (in-process default hooks)", () => {
|
|
|
136
142
|
await getHooksFor("user-prompt-submit", { conversationId: "c1" }),
|
|
137
143
|
).toHaveLength(0);
|
|
138
144
|
});
|
|
145
|
+
|
|
146
|
+
test("getUserHooksFor returns in-process default plugin hooks", async () => {
|
|
147
|
+
registerPlugin(
|
|
148
|
+
buildPlugin("default-memory", {
|
|
149
|
+
"user-prompt-submit": () => Promise.resolve(),
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
expect(await getUserHooksFor("user-prompt-submit")).toHaveLength(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("a first-party default plugin's hooks run even when the chat scope omits it", async () => {
|
|
156
|
+
let ran = 0;
|
|
157
|
+
registerPlugin(
|
|
158
|
+
buildPlugin("default-memory", {
|
|
159
|
+
"user-prompt-submit": () => {
|
|
160
|
+
ran++;
|
|
161
|
+
return Promise.resolve();
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
resolveScopeMock.mockImplementation(() => new Set(["user-a"]));
|
|
166
|
+
|
|
167
|
+
const hooks = await getHooksFor("user-prompt-submit", {
|
|
168
|
+
conversationId: "c1",
|
|
169
|
+
});
|
|
170
|
+
expect(hooks).toHaveLength(1);
|
|
171
|
+
await hooks[0]!({});
|
|
172
|
+
expect(ran).toBe(1);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("a workspace-disabled first-party default is still excluded", async () => {
|
|
176
|
+
registerPlugin(
|
|
177
|
+
buildPlugin("default-memory", {
|
|
178
|
+
"user-prompt-submit": () => Promise.resolve(),
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
const dir = join(getWorkspacePluginsDir(), "default-memory");
|
|
182
|
+
mkdirSync(dir, { recursive: true });
|
|
183
|
+
writeFileSync(join(dir, ".disabled"), "");
|
|
184
|
+
try {
|
|
185
|
+
expect(await getHooksFor("user-prompt-submit")).toHaveLength(0);
|
|
186
|
+
expect(await getUserHooksFor("user-prompt-submit")).toHaveLength(0);
|
|
187
|
+
} finally {
|
|
188
|
+
rmSync(dir, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
139
191
|
});
|
|
140
192
|
|
|
141
193
|
describe("collectUserHookEntries per-chat plugin scope (user-land hooks)", () => {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getCurrentPluginName,
|
|
5
|
+
runInPluginContext,
|
|
6
|
+
runOutsidePluginContext,
|
|
7
|
+
} from "../plugins/plugin-execution-context.js";
|
|
8
|
+
|
|
9
|
+
describe("plugin execution context", () => {
|
|
10
|
+
test("reports no plugin outside any context", () => {
|
|
11
|
+
expect(getCurrentPluginName()).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("marks the plugin for the duration of the call", () => {
|
|
15
|
+
const seen = runInPluginContext("acme", () => getCurrentPluginName());
|
|
16
|
+
expect(seen).toBe("acme");
|
|
17
|
+
expect(getCurrentPluginName()).toBeUndefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("carries across await boundaries", async () => {
|
|
21
|
+
const seen = await runInPluginContext("acme", async () => {
|
|
22
|
+
await Promise.resolve();
|
|
23
|
+
return getCurrentPluginName();
|
|
24
|
+
});
|
|
25
|
+
expect(seen).toBe("acme");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("the innermost plugin wins when contexts nest", () => {
|
|
29
|
+
const seen = runInPluginContext("outer", () =>
|
|
30
|
+
runInPluginContext("inner", () => getCurrentPluginName()),
|
|
31
|
+
);
|
|
32
|
+
expect(seen).toBe("inner");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("runOutsidePluginContext", () => {
|
|
36
|
+
// Host code reached from a plugin (a default tool run during a turn a
|
|
37
|
+
// plugin route started) is not the plugin's code and must not borrow its
|
|
38
|
+
// identity, or a scoped API would attribute that work to the plugin.
|
|
39
|
+
test("clears a surrounding plugin context", () => {
|
|
40
|
+
const seen = runInPluginContext("acme", () =>
|
|
41
|
+
runOutsidePluginContext(() => getCurrentPluginName()),
|
|
42
|
+
);
|
|
43
|
+
expect(seen).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("keeps the context cleared across await boundaries", async () => {
|
|
47
|
+
const seen = await runInPluginContext("acme", () =>
|
|
48
|
+
runOutsidePluginContext(async () => {
|
|
49
|
+
await Promise.resolve();
|
|
50
|
+
return getCurrentPluginName();
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
53
|
+
expect(seen).toBeUndefined();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("restores the plugin context for work after it", () => {
|
|
57
|
+
const seen = runInPluginContext("acme", () => {
|
|
58
|
+
runOutsidePluginContext(() => getCurrentPluginName());
|
|
59
|
+
return getCurrentPluginName();
|
|
60
|
+
});
|
|
61
|
+
expect(seen).toBe("acme");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("a plugin context inside it scopes to the inner plugin", () => {
|
|
65
|
+
const seen = runInPluginContext("acme", () =>
|
|
66
|
+
runOutsidePluginContext(() =>
|
|
67
|
+
runInPluginContext("other", () => getCurrentPluginName()),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
expect(seen).toBe("other");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -99,7 +99,6 @@ const BASELINE: Record<string, readonly string[]> = {
|
|
|
99
99
|
"../../../../../security/untrusted-content.js",
|
|
100
100
|
"../../../../api/events/memory-recalled.js",
|
|
101
101
|
"../../../../api/responses/memory-v3-selection-log.js",
|
|
102
|
-
"../../../../channels/types.js",
|
|
103
102
|
"../../../../config/assistant-feature-flags.js",
|
|
104
103
|
"../../../../config/default-profile-catalog.js",
|
|
105
104
|
"../../../../config/loader.js",
|
|
@@ -99,7 +99,6 @@ const BASELINE: Record<string, readonly string[]> = {
|
|
|
99
99
|
"src/daemon/handlers/skills.ts",
|
|
100
100
|
"src/daemon/skill-memory-refresh.ts",
|
|
101
101
|
"src/daemon/tool-side-effects.ts",
|
|
102
|
-
"src/daemon/trust-context.ts",
|
|
103
102
|
"src/home/feed-source-enrichment.ts",
|
|
104
103
|
"src/permissions/checker.ts",
|
|
105
104
|
"src/persistence/conversation-crud.ts",
|