@vellumai/assistant 0.6.5 → 0.6.6
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 +9 -1
- package/ARCHITECTURE.md +15 -17
- package/Dockerfile +6 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/docs/architecture/integrations.md +32 -39
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +7 -6
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/plugins.md +761 -0
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +212 -68
- package/package.json +1 -1
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +7 -2
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/catalog-cache.test.ts +69 -0
- package/src/__tests__/checker.test.ts +459 -171
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema.test.ts +22 -9
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-window-manager.test.ts +355 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
- package/src/__tests__/conversation-agent-loop.test.ts +30 -141
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
- package/src/__tests__/conversation-process-callsite.test.ts +3 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
- package/src/__tests__/conversation-queue.test.ts +29 -14
- package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
- package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-slash-queue.test.ts +7 -2
- package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
- package/src/__tests__/conversation-speed-override.test.ts +6 -1
- package/src/__tests__/conversation-title-service.test.ts +116 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-usage.test.ts +1 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +2 -2
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +0 -26
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/migration-import-from-url.test.ts +5 -68
- package/src/__tests__/model-intents.test.ts +4 -2
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +41 -76
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +44 -12
- package/src/__tests__/proxy-approval-callback.test.ts +69 -8
- package/src/__tests__/reaction-persistence.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/suggestion-routes.test.ts +103 -4
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-policy.test.ts +21 -3
- package/src/agent/loop.ts +340 -102
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
- package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -9
- package/src/cli/commands/conversations.ts +55 -7
- package/src/cli/commands/image-generation.ts +33 -34
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/program.ts +25 -29
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-tool-registry.ts +0 -15
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +19 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/llm.ts +2 -3
- package/src/config/schemas/security.ts +6 -6
- package/src/config/schemas/tts.ts +11 -0
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +94 -5
- package/src/context/__tests__/compact-prompt.test.ts +27 -9
- package/src/context/prompts/compact.md +26 -12
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +190 -16
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/config-watcher.ts +0 -2
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
- package/src/daemon/conversation-agent-loop.ts +984 -683
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +37 -19
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +14 -7
- package/src/daemon/conversation-runtime-assembly.ts +532 -411
- package/src/daemon/conversation-tool-setup.ts +41 -4
- package/src/daemon/conversation.ts +80 -35
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/handlers/config-model.ts +11 -0
- package/src/daemon/handlers/skills.ts +5 -1
- package/src/daemon/lifecycle.ts +33 -68
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +49 -0
- package/src/daemon/message-types/messages.ts +12 -0
- package/src/daemon/server.ts +5 -3
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -56
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +24 -1
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/emit-feed-event.ts +7 -0
- package/src/home/feed-types.ts +41 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/ipc/__tests__/socket-path.test.ts +11 -50
- package/src/ipc/cli-client.ts +1 -1
- package/src/ipc/cli-server.ts +3 -3
- package/src/ipc/gateway-client.ts +4 -1
- package/src/ipc/routes/browser-context.ts +2 -0
- package/src/ipc/routes/browser.ts +1 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +14 -0
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/socket-path.ts +14 -38
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/conversation-crud.ts +48 -18
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +25 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/extraction.ts +10 -2
- package/src/memory/graph/graph-search.test.ts +1 -0
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/retriever.ts +10 -3
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/pkb/pkb-index.test.ts +1 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +65 -4
- package/src/memory/pkb/pkb-search.ts +40 -18
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +25 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
- package/src/messaging/providers/slack/render-transcript.ts +58 -0
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +30 -14
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -108
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +293 -18
- package/src/permissions/approval-policy.ts +110 -58
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +414 -2
- package/src/permissions/bash-risk-classifier.ts +303 -60
- package/src/permissions/checker.ts +157 -29
- package/src/permissions/command-registry.test.ts +239 -0
- package/src/permissions/command-registry.ts +234 -54
- package/src/permissions/defaults.ts +5 -4
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +61 -4
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/types.ts +2 -0
- package/src/permissions/workspace-policy.ts +8 -3
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/providers/model-catalog.ts +52 -29
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
- package/src/providers/speech-to-text/xai-realtime.ts +39 -14
- package/src/runtime/AGENTS.md +25 -16
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/http-server.ts +77 -8
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
- package/src/runtime/routes/approval-routes.ts +17 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/conversation-routes.ts +223 -116
- package/src/runtime/routes/inbound-message-handler.ts +88 -13
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +0 -3
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/skill-route-registry.ts +75 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +48 -8
- package/src/skills/catalog-cache.ts +12 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
- package/src/tools/browser/browser-execution.ts +88 -19
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/executor.ts +126 -74
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/permission-checker.ts +98 -49
- package/src/tools/policy-context.ts +4 -0
- package/src/tools/registry.ts +140 -3
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/types.ts +28 -2
- package/src/util/platform.ts +7 -2
- package/src/util/pricing.ts +26 -3
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- package/src/tools/watch/watch-state.ts +0 -142
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for plugin-contributed skills (PR 33).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - A plugin declaring `skills: [...]` has its entries registered after
|
|
6
|
+
* bootstrap's `init()` succeeds.
|
|
7
|
+
* - The registered skill is discoverable via `loadSkillCatalog` and
|
|
8
|
+
* resolvable via `loadSkillBySelector` (the exact entry points the model's
|
|
9
|
+
* `skill_load` / `skill_execute` flow use).
|
|
10
|
+
* - Shutdown (runShutdownHooks) unregisters the plugin's skills so repeated
|
|
11
|
+
* bootstraps don't leak catalog entries.
|
|
12
|
+
* - Ref-counted register/unregister semantics match the tool registry's
|
|
13
|
+
* per-skill-id semantics (PR 13 precedent).
|
|
14
|
+
*
|
|
15
|
+
* Strategy mirrors `plugin-bootstrap.test.ts`: stub the credential store so
|
|
16
|
+
* bootstrap doesn't hit real backends, and `resetPluginRegistryForTests` /
|
|
17
|
+
* `resetPluginSkillContributionsForTests` between cases for isolation.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { rm } from "node:fs/promises";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
24
|
+
|
|
25
|
+
// Stub credential store before importing bootstrap so the module captures
|
|
26
|
+
// the fake binding.
|
|
27
|
+
const getSecureKeyAsyncMock = mock(
|
|
28
|
+
async (_account: string): Promise<string | undefined> => undefined,
|
|
29
|
+
);
|
|
30
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
31
|
+
getSecureKeyAsync: getSecureKeyAsyncMock,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
import type { AssistantConfig } from "../config/schema.js";
|
|
35
|
+
import { loadSkillBySelector, loadSkillCatalog } from "../config/skills.js";
|
|
36
|
+
import {
|
|
37
|
+
bootstrapPlugins,
|
|
38
|
+
type DaemonContext,
|
|
39
|
+
} from "../daemon/external-plugins-bootstrap.js";
|
|
40
|
+
import { runShutdownHooks } from "../daemon/shutdown-registry.js";
|
|
41
|
+
import {
|
|
42
|
+
getPluginContributedSkillDefinition,
|
|
43
|
+
getPluginContributedSkillSummaries,
|
|
44
|
+
getPluginSkillRefCount,
|
|
45
|
+
registerPluginSkills,
|
|
46
|
+
resetPluginSkillContributionsForTests,
|
|
47
|
+
unregisterPluginSkills,
|
|
48
|
+
} from "../plugins/plugin-skill-contributions.js";
|
|
49
|
+
import {
|
|
50
|
+
registerPlugin,
|
|
51
|
+
resetPluginRegistryForTests,
|
|
52
|
+
} from "../plugins/registry.js";
|
|
53
|
+
import {
|
|
54
|
+
type Plugin,
|
|
55
|
+
PluginExecutionError,
|
|
56
|
+
type PluginSkillRegistration,
|
|
57
|
+
} from "../plugins/types.js";
|
|
58
|
+
|
|
59
|
+
// Per-process temp tree so bootstrap's plugin-storage directory creation
|
|
60
|
+
// doesn't touch the developer's real ~/.vellum.
|
|
61
|
+
const TEST_INSTANCE_DIR = join(
|
|
62
|
+
tmpdir(),
|
|
63
|
+
`vellum-plugin-skill-test-${process.pid}`,
|
|
64
|
+
);
|
|
65
|
+
process.env.BASE_DATA_DIR = TEST_INSTANCE_DIR;
|
|
66
|
+
|
|
67
|
+
const fakeConfig = {} as unknown as AssistantConfig;
|
|
68
|
+
const fakeCtx: DaemonContext = {
|
|
69
|
+
config: fakeConfig,
|
|
70
|
+
assistantVersion: "9.9.9-test",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Build a plugin that contributes one or more skills. */
|
|
74
|
+
function buildSkillPlugin(
|
|
75
|
+
name: string,
|
|
76
|
+
skills: PluginSkillRegistration[],
|
|
77
|
+
extras: Partial<Omit<Plugin, "manifest" | "skills">> = {},
|
|
78
|
+
): Plugin {
|
|
79
|
+
return {
|
|
80
|
+
manifest: {
|
|
81
|
+
name,
|
|
82
|
+
version: "0.0.1",
|
|
83
|
+
requires: { pluginRuntime: "v1" },
|
|
84
|
+
},
|
|
85
|
+
skills,
|
|
86
|
+
...extras,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe("plugin skill contributions", () => {
|
|
91
|
+
beforeEach(async () => {
|
|
92
|
+
resetPluginRegistryForTests();
|
|
93
|
+
resetPluginSkillContributionsForTests();
|
|
94
|
+
getSecureKeyAsyncMock.mockReset();
|
|
95
|
+
getSecureKeyAsyncMock.mockImplementation(async () => undefined);
|
|
96
|
+
await rm(TEST_INSTANCE_DIR, { recursive: true, force: true });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("plugin skills are registered after bootstrap and exposed by the catalog", async () => {
|
|
100
|
+
const skill: PluginSkillRegistration = {
|
|
101
|
+
id: "plugin-demo-skill",
|
|
102
|
+
name: "plugin-demo",
|
|
103
|
+
displayName: "Plugin Demo",
|
|
104
|
+
description: "A skill contributed by a plugin",
|
|
105
|
+
body: "# Plugin Demo\n\nThis is the plugin-provided body.",
|
|
106
|
+
activationHints: ["demo", "plugin"],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
registerPlugin(buildSkillPlugin("demo-plugin", [skill]));
|
|
110
|
+
await bootstrapPlugins(fakeCtx);
|
|
111
|
+
|
|
112
|
+
// Ref count bumped to exactly 1 so we can tell register and unregister
|
|
113
|
+
// are balanced downstream.
|
|
114
|
+
expect(getPluginSkillRefCount("demo-plugin")).toBe(1);
|
|
115
|
+
|
|
116
|
+
// In-memory registry query surfaces the summary form.
|
|
117
|
+
const summaries = getPluginContributedSkillSummaries();
|
|
118
|
+
const registered = summaries.find((s) => s.id === "plugin-demo-skill");
|
|
119
|
+
expect(registered).toBeDefined();
|
|
120
|
+
expect(registered?.source).toBe("plugin");
|
|
121
|
+
expect(registered?.name).toBe("plugin-demo");
|
|
122
|
+
expect(registered?.displayName).toBe("Plugin Demo");
|
|
123
|
+
expect(registered?.activationHints).toEqual(["demo", "plugin"]);
|
|
124
|
+
// Summary must not carry the body — bodies are definition-only.
|
|
125
|
+
expect((registered as unknown as { body?: unknown }).body).toBeUndefined();
|
|
126
|
+
|
|
127
|
+
// The full definition is retrievable by id.
|
|
128
|
+
const def = getPluginContributedSkillDefinition("plugin-demo-skill");
|
|
129
|
+
expect(def).toBeDefined();
|
|
130
|
+
expect(def?.body).toContain("This is the plugin-provided body");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("catalog and loadSkillBySelector discover plugin-contributed skills (skill_load pathway)", async () => {
|
|
134
|
+
const skill: PluginSkillRegistration = {
|
|
135
|
+
id: "catalog-visible-skill",
|
|
136
|
+
name: "catalog-visible",
|
|
137
|
+
description: "A plugin skill expected to surface in loadSkillCatalog",
|
|
138
|
+
body: "# Catalog-Visible Skill\n\nBody content for skill_load.",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
registerPlugin(buildSkillPlugin("catalog-plugin", [skill]));
|
|
142
|
+
await bootstrapPlugins(fakeCtx);
|
|
143
|
+
|
|
144
|
+
// loadSkillCatalog is the exact entry point `skill_load` consults via
|
|
145
|
+
// `loadSkillBySelector` -> `resolveSkillSelector`.
|
|
146
|
+
const catalog = loadSkillCatalog();
|
|
147
|
+
const found = catalog.find((s) => s.id === "catalog-visible-skill");
|
|
148
|
+
expect(found).toBeDefined();
|
|
149
|
+
expect(found?.source).toBe("plugin");
|
|
150
|
+
expect(found?.description).toBe(skill.description);
|
|
151
|
+
|
|
152
|
+
// loadSkillBySelector is what SkillLoadTool.execute calls — it must
|
|
153
|
+
// return a fully-populated SkillDefinition, including the body.
|
|
154
|
+
const lookup = loadSkillBySelector("catalog-visible-skill");
|
|
155
|
+
expect(lookup.error).toBeUndefined();
|
|
156
|
+
expect(lookup.skill).toBeDefined();
|
|
157
|
+
expect(lookup.skill?.id).toBe("catalog-visible-skill");
|
|
158
|
+
expect(lookup.skill?.body).toContain("Body content for skill_load");
|
|
159
|
+
|
|
160
|
+
// And name-based resolution (what users type when the model picks a
|
|
161
|
+
// skill by name) works the same way.
|
|
162
|
+
const byName = loadSkillBySelector("catalog-visible");
|
|
163
|
+
expect(byName.skill?.id).toBe("catalog-visible-skill");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("shutdown unregisters plugin skills so the catalog no longer lists them", async () => {
|
|
167
|
+
const skill: PluginSkillRegistration = {
|
|
168
|
+
id: "ephemeral-skill",
|
|
169
|
+
name: "ephemeral",
|
|
170
|
+
description: "Disappears after shutdown",
|
|
171
|
+
body: "Only visible while the plugin is alive.",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
registerPlugin(buildSkillPlugin("ephemeral-plugin", [skill]));
|
|
175
|
+
await bootstrapPlugins(fakeCtx);
|
|
176
|
+
|
|
177
|
+
// Sanity: present before shutdown.
|
|
178
|
+
expect(loadSkillCatalog().some((s) => s.id === "ephemeral-skill")).toBe(
|
|
179
|
+
true,
|
|
180
|
+
);
|
|
181
|
+
expect(getPluginSkillRefCount("ephemeral-plugin")).toBe(1);
|
|
182
|
+
|
|
183
|
+
await runShutdownHooks("test-shutdown");
|
|
184
|
+
|
|
185
|
+
// After shutdown, the plugin's skill is gone from both the in-memory
|
|
186
|
+
// registry and any catalog view that consults it.
|
|
187
|
+
expect(getPluginSkillRefCount("ephemeral-plugin")).toBe(0);
|
|
188
|
+
expect(
|
|
189
|
+
getPluginContributedSkillDefinition("ephemeral-skill"),
|
|
190
|
+
).toBeUndefined();
|
|
191
|
+
|
|
192
|
+
const catalogAfter = loadSkillCatalog();
|
|
193
|
+
expect(catalogAfter.some((s) => s.id === "ephemeral-skill")).toBe(false);
|
|
194
|
+
|
|
195
|
+
// And the selector lookup the model would use fails closed.
|
|
196
|
+
const lookup = loadSkillBySelector("ephemeral-skill");
|
|
197
|
+
expect(lookup.skill).toBeUndefined();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("bootstrap is a no-op for plugins without a skills list", async () => {
|
|
201
|
+
// A plugin with no `skills` field must not bump ref counts or
|
|
202
|
+
// populate the catalog at all.
|
|
203
|
+
registerPlugin({
|
|
204
|
+
manifest: {
|
|
205
|
+
name: "no-skills-plugin",
|
|
206
|
+
version: "0.0.1",
|
|
207
|
+
requires: { pluginRuntime: "v1" },
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await bootstrapPlugins(fakeCtx);
|
|
212
|
+
|
|
213
|
+
expect(getPluginSkillRefCount("no-skills-plugin")).toBe(0);
|
|
214
|
+
expect(getPluginContributedSkillSummaries()).toEqual([]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("duplicate skill id across plugins fails bootstrap with the plugin named", async () => {
|
|
218
|
+
const shared: PluginSkillRegistration = {
|
|
219
|
+
id: "contested-id",
|
|
220
|
+
name: "contested",
|
|
221
|
+
description: "First",
|
|
222
|
+
body: "from first plugin",
|
|
223
|
+
};
|
|
224
|
+
const duplicate: PluginSkillRegistration = {
|
|
225
|
+
id: "contested-id",
|
|
226
|
+
name: "contested",
|
|
227
|
+
description: "Second",
|
|
228
|
+
body: "from second plugin",
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
registerPlugin(buildSkillPlugin("first-plugin", [shared]));
|
|
232
|
+
registerPlugin(buildSkillPlugin("second-plugin", [duplicate]));
|
|
233
|
+
|
|
234
|
+
let caught: unknown;
|
|
235
|
+
try {
|
|
236
|
+
await bootstrapPlugins(fakeCtx);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
caught = err;
|
|
239
|
+
}
|
|
240
|
+
expect(caught).toBeInstanceOf(PluginExecutionError);
|
|
241
|
+
// The error must identify which plugin tripped the collision so
|
|
242
|
+
// operators can deploy a fix.
|
|
243
|
+
const msg = (caught as PluginExecutionError).message;
|
|
244
|
+
expect(msg).toContain("second-plugin");
|
|
245
|
+
expect(msg).toContain("contested-id");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("intra-batch duplicate skill id in one plugin is rejected at registration", () => {
|
|
249
|
+
// Directly exercise the registry (no bootstrap) — a single plugin
|
|
250
|
+
// declaring the same id twice is a configuration bug and must fail
|
|
251
|
+
// loudly rather than silently overwrite.
|
|
252
|
+
expect(() =>
|
|
253
|
+
registerPluginSkills("dup-plugin", [
|
|
254
|
+
{
|
|
255
|
+
id: "dup-id",
|
|
256
|
+
name: "one",
|
|
257
|
+
description: "first",
|
|
258
|
+
body: "A",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "dup-id",
|
|
262
|
+
name: "two",
|
|
263
|
+
description: "second",
|
|
264
|
+
body: "B",
|
|
265
|
+
},
|
|
266
|
+
]),
|
|
267
|
+
).toThrow(PluginExecutionError);
|
|
268
|
+
expect(() =>
|
|
269
|
+
registerPluginSkills("dup-plugin", [
|
|
270
|
+
{ id: "x", name: "one", description: "a", body: "A" },
|
|
271
|
+
{ id: "x", name: "two", description: "b", body: "B" },
|
|
272
|
+
]),
|
|
273
|
+
).toThrow(/declared skill "x" more than once/);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("ref-counted unregister: second unregister call after repeated registers drops skills", () => {
|
|
277
|
+
const registrations: PluginSkillRegistration[] = [
|
|
278
|
+
{
|
|
279
|
+
id: "refcount-skill",
|
|
280
|
+
name: "refcount",
|
|
281
|
+
description: "Ref-count demo",
|
|
282
|
+
body: "stays until the counter hits zero",
|
|
283
|
+
},
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
registerPluginSkills("refcount-plugin", registrations);
|
|
287
|
+
// Second call is the "same plugin registered again" pattern (hot
|
|
288
|
+
// reload). It must bump the counter without re-inserting.
|
|
289
|
+
registerPluginSkills("refcount-plugin", registrations);
|
|
290
|
+
expect(getPluginSkillRefCount("refcount-plugin")).toBe(2);
|
|
291
|
+
expect(getPluginContributedSkillDefinition("refcount-skill")).toBeDefined();
|
|
292
|
+
|
|
293
|
+
// One unregister decrements only — skill must still be registered.
|
|
294
|
+
unregisterPluginSkills("refcount-plugin");
|
|
295
|
+
expect(getPluginSkillRefCount("refcount-plugin")).toBe(1);
|
|
296
|
+
expect(getPluginContributedSkillDefinition("refcount-skill")).toBeDefined();
|
|
297
|
+
|
|
298
|
+
// Second unregister drops the entry for real.
|
|
299
|
+
unregisterPluginSkills("refcount-plugin");
|
|
300
|
+
expect(getPluginSkillRefCount("refcount-plugin")).toBe(0);
|
|
301
|
+
expect(
|
|
302
|
+
getPluginContributedSkillDefinition("refcount-skill"),
|
|
303
|
+
).toBeUndefined();
|
|
304
|
+
|
|
305
|
+
// Third unregister (over-decrement) is a no-op, not a throw — mirrors
|
|
306
|
+
// the tool-registry behavior so shutdown races don't crash the
|
|
307
|
+
// daemon.
|
|
308
|
+
expect(() => unregisterPluginSkills("refcount-plugin")).not.toThrow();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("managed (filesystem) skill with the same id overrides a plugin-contributed skill", async () => {
|
|
312
|
+
// Plugin skills sit below managed/workspace in the catalog precedence
|
|
313
|
+
// chain so a user can shadow a plugin-provided skill by dropping a
|
|
314
|
+
// SKILL.md with the same id under ~/.vellum/workspace/skills. The test
|
|
315
|
+
// harness configures VELLUM_WORKSPACE_DIR via the bun test preload, so
|
|
316
|
+
// we can synthesize a filesystem skill for the same id and assert it
|
|
317
|
+
// wins.
|
|
318
|
+
const { mkdirSync, writeFileSync, rmSync, existsSync } =
|
|
319
|
+
await import("node:fs");
|
|
320
|
+
|
|
321
|
+
const workspaceDir = process.env.VELLUM_WORKSPACE_DIR;
|
|
322
|
+
// Skip the filesystem override check when the harness did not provide a
|
|
323
|
+
// workspace dir — the other tests in this file still cover catalog
|
|
324
|
+
// visibility, which is the core of PR 33.
|
|
325
|
+
if (!workspaceDir) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const skillsDir = join(workspaceDir, "skills", "shared-id");
|
|
330
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
331
|
+
writeFileSync(
|
|
332
|
+
join(skillsDir, "SKILL.md"),
|
|
333
|
+
[
|
|
334
|
+
"---",
|
|
335
|
+
'name: "filesystem-version"',
|
|
336
|
+
'description: "User-authored override"',
|
|
337
|
+
"---",
|
|
338
|
+
"",
|
|
339
|
+
"# Filesystem Override",
|
|
340
|
+
].join("\n"),
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
const pluginSkill: PluginSkillRegistration = {
|
|
344
|
+
id: "shared-id",
|
|
345
|
+
name: "plugin-version",
|
|
346
|
+
description: "Plugin-provided skill that should be shadowed",
|
|
347
|
+
body: "plugin body",
|
|
348
|
+
};
|
|
349
|
+
registerPlugin(buildSkillPlugin("shadow-plugin", [pluginSkill]));
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
await bootstrapPlugins(fakeCtx);
|
|
353
|
+
|
|
354
|
+
const catalog = loadSkillCatalog();
|
|
355
|
+
const entry = catalog.find((s) => s.id === "shared-id");
|
|
356
|
+
// The filesystem SKILL.md wins — source flips to "managed" and the
|
|
357
|
+
// plugin's metadata is not what we see.
|
|
358
|
+
expect(entry).toBeDefined();
|
|
359
|
+
expect(entry?.source).toBe("managed");
|
|
360
|
+
expect(entry?.name).toBe("filesystem-version");
|
|
361
|
+
} finally {
|
|
362
|
+
if (existsSync(skillsDir)) {
|
|
363
|
+
rmSync(skillsDir, { recursive: true, force: true });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for plugin tool contributions (PR 31).
|
|
3
|
+
*
|
|
4
|
+
* Covers the end-to-end flow that lets a plugin declare tools on its
|
|
5
|
+
* manifest and have them surface through the global tool registry:
|
|
6
|
+
*
|
|
7
|
+
* - Registering a plugin with `tools: Tool[]`, running `bootstrapPlugins`,
|
|
8
|
+
* and observing the contributed tool via `getAllTools()` / `getTool()`.
|
|
9
|
+
* - Tool ownership metadata (`origin: "plugin"`, `ownerPluginId: <plugin>`)
|
|
10
|
+
* stamped authoritatively by `registerPluginTools` regardless of what the
|
|
11
|
+
* plugin author set on the incoming object.
|
|
12
|
+
* - Shutdown hook unregistering the contributed tools so the registry is
|
|
13
|
+
* clean again after teardown.
|
|
14
|
+
* - Direct `registerPluginTools` / `unregisterPluginTools` semantics,
|
|
15
|
+
* including the plugin-scoped ref count.
|
|
16
|
+
*
|
|
17
|
+
* Uses `mock.module` to stub the credential store so bootstrap doesn't hit
|
|
18
|
+
* the real backend. `resetPluginRegistryForTests()` and
|
|
19
|
+
* `__clearRegistryForTesting()` isolate registry state between cases so
|
|
20
|
+
* this file can run alongside other plugin/tool-registry tests without
|
|
21
|
+
* cross-contamination.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { rm } from "node:fs/promises";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
28
|
+
|
|
29
|
+
// Mock the credential store before importing the bootstrap so the module
|
|
30
|
+
// under test captures the stubbed binding. Bootstrap only calls this for
|
|
31
|
+
// plugins that declare `requiresCredential`; the tests in this file don't,
|
|
32
|
+
// so the stub simply returns undefined.
|
|
33
|
+
const getSecureKeyAsyncMock = mock(
|
|
34
|
+
async (_account: string): Promise<string | undefined> => undefined,
|
|
35
|
+
);
|
|
36
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
37
|
+
getSecureKeyAsync: getSecureKeyAsyncMock,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
import type { AssistantConfig } from "../config/schema.js";
|
|
41
|
+
import {
|
|
42
|
+
bootstrapPlugins,
|
|
43
|
+
type DaemonContext,
|
|
44
|
+
} from "../daemon/external-plugins-bootstrap.js";
|
|
45
|
+
import { runShutdownHooks } from "../daemon/shutdown-registry.js";
|
|
46
|
+
import { RiskLevel } from "../permissions/types.js";
|
|
47
|
+
import {
|
|
48
|
+
registerPlugin,
|
|
49
|
+
resetPluginRegistryForTests,
|
|
50
|
+
} from "../plugins/registry.js";
|
|
51
|
+
import type { Plugin } from "../plugins/types.js";
|
|
52
|
+
import type { ToolDefinition } from "../providers/types.js";
|
|
53
|
+
import {
|
|
54
|
+
__clearRegistryForTesting,
|
|
55
|
+
__resetRegistryForTesting,
|
|
56
|
+
getAllTools,
|
|
57
|
+
getPluginRefCount,
|
|
58
|
+
getTool,
|
|
59
|
+
registerPluginTools,
|
|
60
|
+
unregisterPluginTools,
|
|
61
|
+
} from "../tools/registry.js";
|
|
62
|
+
import type { Tool, ToolContext, ToolExecutionResult } from "../tools/types.js";
|
|
63
|
+
|
|
64
|
+
// Redirect plugin-storage-directory creation into a per-process temp tree so
|
|
65
|
+
// the test doesn't touch the developer's real ~/.vellum. This matches the
|
|
66
|
+
// convention used by plugin-bootstrap.test.ts.
|
|
67
|
+
const TEST_INSTANCE_DIR = join(
|
|
68
|
+
tmpdir(),
|
|
69
|
+
`vellum-plugin-tool-contrib-test-${process.pid}`,
|
|
70
|
+
);
|
|
71
|
+
process.env.BASE_DATA_DIR = TEST_INSTANCE_DIR;
|
|
72
|
+
|
|
73
|
+
const fakeConfig = {} as unknown as AssistantConfig;
|
|
74
|
+
const fakeCtx: DaemonContext = {
|
|
75
|
+
config: fakeConfig,
|
|
76
|
+
assistantVersion: "9.9.9-test",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function makeFakeTool(name: string, extras: Partial<Tool> = {}): Tool {
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
description: `Fake ${name}`,
|
|
83
|
+
category: "plugin-test",
|
|
84
|
+
defaultRiskLevel: RiskLevel.Low,
|
|
85
|
+
getDefinition(): ToolDefinition {
|
|
86
|
+
return {
|
|
87
|
+
name,
|
|
88
|
+
description: `Fake ${name}`,
|
|
89
|
+
input_schema: { type: "object", properties: {}, required: [] },
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
async execute(
|
|
93
|
+
_input: Record<string, unknown>,
|
|
94
|
+
_context: ToolContext,
|
|
95
|
+
): Promise<ToolExecutionResult> {
|
|
96
|
+
return { content: "ok", isError: false };
|
|
97
|
+
},
|
|
98
|
+
...extras,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildPlugin(
|
|
103
|
+
name: string,
|
|
104
|
+
extras: Partial<Omit<Plugin, "manifest">> = {},
|
|
105
|
+
): Plugin {
|
|
106
|
+
return {
|
|
107
|
+
manifest: {
|
|
108
|
+
name,
|
|
109
|
+
version: "0.0.1",
|
|
110
|
+
requires: { pluginRuntime: "v1" },
|
|
111
|
+
},
|
|
112
|
+
...extras,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe("plugin tool contributions", () => {
|
|
117
|
+
beforeEach(async () => {
|
|
118
|
+
resetPluginRegistryForTests();
|
|
119
|
+
// Clear the tool registry completely so we can make vacuous-free
|
|
120
|
+
// assertions about which tools are present. We don't need any of the
|
|
121
|
+
// eager/host tools for these tests.
|
|
122
|
+
__clearRegistryForTesting();
|
|
123
|
+
getSecureKeyAsyncMock.mockReset();
|
|
124
|
+
getSecureKeyAsyncMock.mockImplementation(async () => undefined);
|
|
125
|
+
await rm(TEST_INSTANCE_DIR, { recursive: true, force: true });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("bootstrap registers plugin tools and makes them discoverable", async () => {
|
|
129
|
+
const tool = makeFakeTool("plugin-contrib-tool");
|
|
130
|
+
const plugin = buildPlugin("alpha-contributor", {
|
|
131
|
+
async init() {},
|
|
132
|
+
tools: [tool],
|
|
133
|
+
});
|
|
134
|
+
registerPlugin(plugin);
|
|
135
|
+
|
|
136
|
+
await bootstrapPlugins(fakeCtx);
|
|
137
|
+
|
|
138
|
+
const retrieved = getTool("plugin-contrib-tool");
|
|
139
|
+
expect(retrieved).toBeDefined();
|
|
140
|
+
// Ownership metadata must be stamped authoritatively by the bootstrap —
|
|
141
|
+
// the registry uses it to drive ref-counting and conflict detection when
|
|
142
|
+
// the plugin shuts down or is hot-reloaded. Plugin tools live in their
|
|
143
|
+
// own `origin: "plugin"` namespace, disjoint from real skills, so a
|
|
144
|
+
// plugin name that happens to match a skill id cannot collide.
|
|
145
|
+
expect(retrieved?.origin).toBe("plugin");
|
|
146
|
+
expect(retrieved?.ownerPluginId).toBe("alpha-contributor");
|
|
147
|
+
|
|
148
|
+
// The tool surfaces in the global `getAllTools()` snapshot, which is
|
|
149
|
+
// what downstream consumers (tool-manifest, session projection) read.
|
|
150
|
+
const names = getAllTools().map((t) => t.name);
|
|
151
|
+
expect(names).toContain("plugin-contrib-tool");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("plugin tools are unregistered when shutdown hooks run", async () => {
|
|
155
|
+
const plugin = buildPlugin("bravo-contributor", {
|
|
156
|
+
async init() {},
|
|
157
|
+
tools: [makeFakeTool("bravo-tool")],
|
|
158
|
+
});
|
|
159
|
+
registerPlugin(plugin);
|
|
160
|
+
|
|
161
|
+
await bootstrapPlugins(fakeCtx);
|
|
162
|
+
expect(getTool("bravo-tool")).toBeDefined();
|
|
163
|
+
|
|
164
|
+
await runShutdownHooks("test-shutdown");
|
|
165
|
+
|
|
166
|
+
expect(getTool("bravo-tool")).toBeUndefined();
|
|
167
|
+
expect(getPluginRefCount("bravo-contributor")).toBe(0);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("bootstrap is a no-op for plugins that declare no tools", async () => {
|
|
171
|
+
const plugin = buildPlugin("no-tools", { async init() {} });
|
|
172
|
+
registerPlugin(plugin);
|
|
173
|
+
|
|
174
|
+
await bootstrapPlugins(fakeCtx);
|
|
175
|
+
// No tool should have been registered.
|
|
176
|
+
expect(getAllTools()).toHaveLength(0);
|
|
177
|
+
|
|
178
|
+
// Shutdown must also be safe — `unregisterPluginTools` is idempotent for
|
|
179
|
+
// plugins that never contributed any tools.
|
|
180
|
+
await runShutdownHooks("test-shutdown");
|
|
181
|
+
expect(getAllTools()).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("tools declared before init() runs are only visible after bootstrap", async () => {
|
|
185
|
+
// Registration alone must not touch the tool registry — only the
|
|
186
|
+
// bootstrap pass does. This matters because `bootstrapPlugins` runs once
|
|
187
|
+
// at daemon startup after the plugin registry is populated; if
|
|
188
|
+
// registration itself contributed tools, hot-reloading a plugin module
|
|
189
|
+
// during boot would race with `initializeTools()`.
|
|
190
|
+
const plugin = buildPlugin("charlie-contributor", {
|
|
191
|
+
async init() {},
|
|
192
|
+
tools: [makeFakeTool("charlie-tool")],
|
|
193
|
+
});
|
|
194
|
+
registerPlugin(plugin);
|
|
195
|
+
|
|
196
|
+
expect(getTool("charlie-tool")).toBeUndefined();
|
|
197
|
+
|
|
198
|
+
await bootstrapPlugins(fakeCtx);
|
|
199
|
+
expect(getTool("charlie-tool")).toBeDefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("tools are only registered after init() succeeds", async () => {
|
|
203
|
+
// A plugin whose init throws must not contribute tools — the bootstrap
|
|
204
|
+
// aborts with a PluginExecutionError, and nothing from this plugin
|
|
205
|
+
// should leak into the tool registry.
|
|
206
|
+
const plugin = buildPlugin("delta-broken", {
|
|
207
|
+
async init() {
|
|
208
|
+
throw new Error("boom");
|
|
209
|
+
},
|
|
210
|
+
tools: [makeFakeTool("delta-tool")],
|
|
211
|
+
});
|
|
212
|
+
registerPlugin(plugin);
|
|
213
|
+
|
|
214
|
+
await expect(bootstrapPlugins(fakeCtx)).rejects.toThrow(/delta-broken/);
|
|
215
|
+
expect(getTool("delta-tool")).toBeUndefined();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe("registerPluginTools / unregisterPluginTools helpers", () => {
|
|
220
|
+
beforeEach(() => {
|
|
221
|
+
__resetRegistryForTesting();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("registerPluginTools stamps origin and ownerPluginId from the plugin name", () => {
|
|
225
|
+
// Even if the plugin author hands in a tool with no ownership metadata,
|
|
226
|
+
// the helper fills it in so the tool can be unregistered later.
|
|
227
|
+
const accepted = registerPluginTools("my-plugin", [
|
|
228
|
+
makeFakeTool("pt_stamped"),
|
|
229
|
+
]);
|
|
230
|
+
expect(accepted).toHaveLength(1);
|
|
231
|
+
expect(accepted[0]?.origin).toBe("plugin");
|
|
232
|
+
expect(accepted[0]?.ownerPluginId).toBe("my-plugin");
|
|
233
|
+
|
|
234
|
+
const retrieved = getTool("pt_stamped");
|
|
235
|
+
expect(retrieved?.origin).toBe("plugin");
|
|
236
|
+
expect(retrieved?.ownerPluginId).toBe("my-plugin");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("registerPluginTools overwrites any pre-existing ownership metadata", () => {
|
|
240
|
+
// A plugin author could (maliciously or mistakenly) hand in a tool
|
|
241
|
+
// pre-tagged with another skill's or plugin's ID. The helper must
|
|
242
|
+
// overwrite it so the bootstrap is always the source of truth for
|
|
243
|
+
// ownership — and it must clear cross-origin fields (ownerSkillId /
|
|
244
|
+
// ownerMcpServerId) so the stamped tool cannot leak across namespaces.
|
|
245
|
+
const spoofed = makeFakeTool("pt_spoof", {
|
|
246
|
+
origin: "skill",
|
|
247
|
+
ownerSkillId: "some-other-skill",
|
|
248
|
+
});
|
|
249
|
+
registerPluginTools("my-plugin", [spoofed]);
|
|
250
|
+
const retrieved = getTool("pt_spoof");
|
|
251
|
+
expect(retrieved?.origin).toBe("plugin");
|
|
252
|
+
expect(retrieved?.ownerPluginId).toBe("my-plugin");
|
|
253
|
+
expect(retrieved?.ownerSkillId).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("unregisterPluginTools removes the plugin's tools", () => {
|
|
257
|
+
registerPluginTools("rm-plugin", [
|
|
258
|
+
makeFakeTool("pt_rm_a"),
|
|
259
|
+
makeFakeTool("pt_rm_b"),
|
|
260
|
+
]);
|
|
261
|
+
expect(getTool("pt_rm_a")).toBeDefined();
|
|
262
|
+
expect(getTool("pt_rm_b")).toBeDefined();
|
|
263
|
+
|
|
264
|
+
unregisterPluginTools("rm-plugin");
|
|
265
|
+
|
|
266
|
+
expect(getTool("pt_rm_a")).toBeUndefined();
|
|
267
|
+
expect(getTool("pt_rm_b")).toBeUndefined();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("unregisterPluginTools is a no-op for plugins that never contributed", () => {
|
|
271
|
+
expect(() => unregisterPluginTools("never-registered")).not.toThrow();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("ref-counting: repeated registrations require matching unregister calls", () => {
|
|
275
|
+
registerPluginTools("rc-plugin", [makeFakeTool("pt_rc")]);
|
|
276
|
+
registerPluginTools("rc-plugin", [makeFakeTool("pt_rc")]);
|
|
277
|
+
expect(getPluginRefCount("rc-plugin")).toBe(2);
|
|
278
|
+
|
|
279
|
+
unregisterPluginTools("rc-plugin");
|
|
280
|
+
expect(getTool("pt_rc")).toBeDefined();
|
|
281
|
+
|
|
282
|
+
unregisterPluginTools("rc-plugin");
|
|
283
|
+
expect(getTool("pt_rc")).toBeUndefined();
|
|
284
|
+
expect(getPluginRefCount("rc-plugin")).toBe(0);
|
|
285
|
+
});
|
|
286
|
+
});
|