@vellumai/assistant 0.8.8 → 0.8.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +235 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
- package/src/__tests__/conversation-agent-loop.test.ts +297 -586
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +225 -9
- package/src/acp/resolve-agent.ts +122 -17
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +414 -248
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/bundled-skills/acp/SKILL.md +85 -33
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +15 -6
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +175 -1055
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +404 -57
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +8 -2
- package/src/tools/acp/spawn.test.ts +176 -195
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tests for {@link installPlugin}.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Two materialization paths are exercised without touching the network or
|
|
5
|
+
* spawning real subprocesses:
|
|
6
|
+
* - First-party plugins are fetched from the GitHub Contents API, replaced
|
|
7
|
+
* by an in-memory fixture passed via the `fetch` dependency.
|
|
8
|
+
* - External (whitelisted) plugins are shallow-cloned with `git`, replaced
|
|
9
|
+
* by a fake {@link GitRunner} that materializes a tree into the clone dir.
|
|
10
|
+
* No globals are monkey-patched and no `--test-hook` exports leak into
|
|
11
|
+
* production code.
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
import {
|
|
@@ -16,92 +21,84 @@ import {
|
|
|
16
21
|
writeFileSync,
|
|
17
22
|
} from "node:fs";
|
|
18
23
|
import { tmpdir } from "node:os";
|
|
19
|
-
import { join } from "node:path";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
20
25
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
21
26
|
|
|
22
27
|
import {
|
|
23
28
|
type FetchLike,
|
|
29
|
+
type GitRunner,
|
|
24
30
|
installPlugin,
|
|
25
31
|
InvalidPluginNameError,
|
|
26
32
|
PluginAlreadyInstalledError,
|
|
27
33
|
PluginNotFoundError,
|
|
34
|
+
PluginSourceUnavailableError,
|
|
28
35
|
sanitizePluginName,
|
|
29
36
|
} from "../install-from-github.js";
|
|
30
37
|
|
|
38
|
+
const CANON_REPO = "vellum-ai/vellum-assistant";
|
|
39
|
+
/** Synthetic host the fixtures use for Contents API `download_url`s. */
|
|
40
|
+
const DOWNLOAD_HOST = "https://files.test/";
|
|
41
|
+
|
|
31
42
|
/**
|
|
32
|
-
* Build a GitHub Contents API
|
|
43
|
+
* Build a `fetch` that serves the GitHub Contents API from an in-memory tree.
|
|
33
44
|
*
|
|
34
|
-
* `tree`
|
|
35
|
-
* `simple-memory/
|
|
36
|
-
*
|
|
37
|
-
*
|
|
45
|
+
* `tree` is keyed by full canonical-repo path (e.g.
|
|
46
|
+
* `experimental/plugins/simple-memory/package.json`) and maps each entry to a
|
|
47
|
+
* file's content (`string`/`Uint8Array`) or `null` for an explicit directory.
|
|
48
|
+
* Directory listings are derived from the key set; file `download_url`s point
|
|
49
|
+
* at {@link DOWNLOAD_HOST} and are served with the stored bytes.
|
|
38
50
|
*
|
|
39
|
-
* The
|
|
40
|
-
*
|
|
41
|
-
* - any other URL we hand out as `download_url`
|
|
51
|
+
* The marketplace manifest lookup is answered with `manifest` (or a 404 when
|
|
52
|
+
* omitted, so resolution degrades to the first-party source).
|
|
42
53
|
*/
|
|
43
|
-
function
|
|
44
|
-
tree: Record<string, Uint8Array | string | null
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
function listing(apiPath: string):
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
function makeContentsFetch(opts: {
|
|
55
|
+
tree: Record<string, Uint8Array | string | null>;
|
|
56
|
+
manifest?: unknown;
|
|
57
|
+
}): FetchLike {
|
|
58
|
+
const MANIFEST_URL = `https://api.github.com/repos/${CANON_REPO}/contents/experimental/plugins/marketplace.json`;
|
|
59
|
+
const CONTENTS = `https://api.github.com/repos/${CANON_REPO}/contents/`;
|
|
60
|
+
const { tree } = opts;
|
|
61
|
+
|
|
62
|
+
function listing(apiPath: string):
|
|
63
|
+
| {
|
|
64
|
+
name: string;
|
|
65
|
+
path: string;
|
|
66
|
+
type: string;
|
|
67
|
+
download_url: string | null;
|
|
68
|
+
}[]
|
|
69
|
+
| null {
|
|
70
|
+
const prefix = apiPath ? `${apiPath}/` : "";
|
|
71
|
+
const direct = new Map<string, boolean>();
|
|
57
72
|
for (const key of Object.keys(tree)) {
|
|
58
73
|
if (!key.startsWith(prefix)) continue;
|
|
59
74
|
const remainder = key.slice(prefix.length);
|
|
60
75
|
if (!remainder) continue;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!direct.has(head!)) direct.set(head!, isDir ? "dir" : "file");
|
|
65
|
-
} else {
|
|
66
|
-
if (!direct.has(head!)) direct.set(head!, "dir");
|
|
67
|
-
}
|
|
76
|
+
const seg = remainder.split("/")[0]!;
|
|
77
|
+
const isDir = remainder.includes("/") || tree[`${prefix}${seg}`] === null;
|
|
78
|
+
direct.set(seg, (direct.get(seg) ?? false) || isDir);
|
|
68
79
|
}
|
|
69
80
|
if (direct.size === 0) return null;
|
|
70
|
-
return Array.from(direct.entries()).map(([name,
|
|
71
|
-
name
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
type,
|
|
77
|
-
size: type === "file" ? (tree[`${prefix}${name}`] as string).length : 0,
|
|
78
|
-
download_url:
|
|
79
|
-
type === "file" ? `${PREFIX_RAW}${prefix}${name}` : null,
|
|
80
|
-
}));
|
|
81
|
+
return Array.from(direct.entries()).map(([name, isDir]) => {
|
|
82
|
+
const path = `${prefix}${name}`;
|
|
83
|
+
return isDir
|
|
84
|
+
? { name, path, type: "dir", download_url: null }
|
|
85
|
+
: { name, path, type: "file", download_url: `${DOWNLOAD_HOST}${path}` };
|
|
86
|
+
});
|
|
81
87
|
}
|
|
82
88
|
|
|
83
|
-
return (async (input: RequestInfo | URL
|
|
89
|
+
return (async (input: RequestInfo | URL) => {
|
|
84
90
|
const url = typeof input === "string" ? input : input.toString();
|
|
85
91
|
|
|
86
|
-
if (url.startsWith(
|
|
87
|
-
|
|
88
|
-
const apiPath = `experimental/plugins/${decodeURIComponent(after)}`;
|
|
89
|
-
const body = listing(apiPath);
|
|
90
|
-
if (body === null) {
|
|
92
|
+
if (url.startsWith(MANIFEST_URL)) {
|
|
93
|
+
if (opts.manifest === undefined) {
|
|
91
94
|
return new Response("not found", { status: 404 });
|
|
92
95
|
}
|
|
93
|
-
return new Response(JSON.stringify(
|
|
94
|
-
status: 200,
|
|
95
|
-
headers: { "content-type": "application/json" },
|
|
96
|
-
});
|
|
96
|
+
return new Response(JSON.stringify(opts.manifest), { status: 200 });
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
if (url.startsWith(
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
decodeURIComponent(url.slice(PREFIX_RAW.length));
|
|
103
|
-
const rel = key.slice("experimental/plugins/".length);
|
|
104
|
-
const file = tree[rel];
|
|
99
|
+
if (url.startsWith(DOWNLOAD_HOST)) {
|
|
100
|
+
const path = decodeURIComponent(url.slice(DOWNLOAD_HOST.length));
|
|
101
|
+
const file = tree[path];
|
|
105
102
|
if (file === null || file === undefined) {
|
|
106
103
|
return new Response("not found", { status: 404 });
|
|
107
104
|
}
|
|
@@ -110,11 +107,82 @@ function fixtureFetch(
|
|
|
110
107
|
return new Response(Buffer.from(bytes), { status: 200 });
|
|
111
108
|
}
|
|
112
109
|
|
|
113
|
-
|
|
110
|
+
if (url.startsWith(CONTENTS)) {
|
|
111
|
+
const after = decodeURIComponent(
|
|
112
|
+
url.slice(CONTENTS.length).split("?")[0]!,
|
|
113
|
+
);
|
|
114
|
+
const body = listing(after);
|
|
115
|
+
if (body === null) {
|
|
116
|
+
return new Response("not found", { status: 404 });
|
|
117
|
+
}
|
|
118
|
+
return new Response(JSON.stringify(body), { status: 200 });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return new Response(`unexpected url: ${url}`, { status: 500 });
|
|
114
122
|
}) as FetchLike;
|
|
115
123
|
}
|
|
116
124
|
|
|
117
|
-
|
|
125
|
+
/** First-party fixture: keys are relative to `experimental/plugins/`. */
|
|
126
|
+
function fixtureFetch(
|
|
127
|
+
tree: Record<string, Uint8Array | string | null>,
|
|
128
|
+
): FetchLike {
|
|
129
|
+
const full: Record<string, Uint8Array | string | null> = {};
|
|
130
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
131
|
+
full[`experimental/plugins/${key}`] = value;
|
|
132
|
+
}
|
|
133
|
+
return makeContentsFetch({ tree: full });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Build a fake {@link GitRunner} that simulates a shallow clone.
|
|
138
|
+
*
|
|
139
|
+
* On `fetch`, the configured `tree` (repo-relative path → content, `null` =
|
|
140
|
+
* directory) plus a token `.git/config` are written into the clone `cwd`, so
|
|
141
|
+
* the subsequent copy can be asserted to include the tree and exclude `.git`.
|
|
142
|
+
* `rev-parse HEAD` returns `commit`. Optional hooks let a test record the git
|
|
143
|
+
* arguments or inject a fetch failure.
|
|
144
|
+
*/
|
|
145
|
+
function fakeGitRunner(opts: {
|
|
146
|
+
tree: Record<string, string | null>;
|
|
147
|
+
commit?: string;
|
|
148
|
+
calls?: string[][];
|
|
149
|
+
fetchError?: Error;
|
|
150
|
+
}): GitRunner {
|
|
151
|
+
const commit = opts.commit ?? "abc123def4567890abc123def4567890abcdef12";
|
|
152
|
+
return async (args, { cwd }) => {
|
|
153
|
+
opts.calls?.push([...args]);
|
|
154
|
+
switch (args[0]) {
|
|
155
|
+
case "fetch": {
|
|
156
|
+
if (opts.fetchError) throw opts.fetchError;
|
|
157
|
+
mkdirSync(join(cwd, ".git"), { recursive: true });
|
|
158
|
+
writeFileSync(join(cwd, ".git", "config"), "[core]\n");
|
|
159
|
+
for (const [rel, content] of Object.entries(opts.tree)) {
|
|
160
|
+
if (content === null) {
|
|
161
|
+
mkdirSync(join(cwd, rel), { recursive: true });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const dest = join(cwd, rel);
|
|
165
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
166
|
+
writeFileSync(dest, content);
|
|
167
|
+
}
|
|
168
|
+
return { stdout: "" };
|
|
169
|
+
}
|
|
170
|
+
case "rev-parse":
|
|
171
|
+
return { stdout: `${commit}\n` };
|
|
172
|
+
default:
|
|
173
|
+
return { stdout: "" };
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** A git runner that fails the test if any git command is invoked. */
|
|
179
|
+
const unusedGitRunner: GitRunner = async (args) => {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`git should not run for a first-party install: ${args.join(" ")}`,
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
describe("installPlugin — first-party", () => {
|
|
118
186
|
let ws: string;
|
|
119
187
|
let pluginsDir: string;
|
|
120
188
|
|
|
@@ -128,7 +196,7 @@ describe("installPlugin", () => {
|
|
|
128
196
|
rmSync(ws, { recursive: true, force: true });
|
|
129
197
|
});
|
|
130
198
|
|
|
131
|
-
test("copies
|
|
199
|
+
test("copies a first-party plugin into <workspacePluginsDir>/<name>", async () => {
|
|
132
200
|
const result = await installPlugin(
|
|
133
201
|
{ name: "simple-memory", force: false, ref: "main" },
|
|
134
202
|
{
|
|
@@ -149,6 +217,8 @@ describe("installPlugin", () => {
|
|
|
149
217
|
expect(result.target).toBe(target);
|
|
150
218
|
expect(result.fileCount).toBe(4);
|
|
151
219
|
expect(result.ref).toBe("main");
|
|
220
|
+
// First-party installs have no clone, so no commit is resolved.
|
|
221
|
+
expect(result.commit).toBeNull();
|
|
152
222
|
expect(existsSync(join(target, "package.json"))).toBe(true);
|
|
153
223
|
expect(existsSync(join(target, "README.md"))).toBe(true);
|
|
154
224
|
expect(existsSync(join(target, "hooks", "init.ts"))).toBe(true);
|
|
@@ -158,6 +228,33 @@ describe("installPlugin", () => {
|
|
|
158
228
|
);
|
|
159
229
|
});
|
|
160
230
|
|
|
231
|
+
test("writes a provenance manifest recording the source and ref", async () => {
|
|
232
|
+
// GIVEN a first-party install
|
|
233
|
+
const target = join(pluginsDir, "simple-memory");
|
|
234
|
+
|
|
235
|
+
// WHEN it completes
|
|
236
|
+
await installPlugin(
|
|
237
|
+
{ name: "simple-memory", force: false, ref: "main" },
|
|
238
|
+
{
|
|
239
|
+
fetch: fixtureFetch({
|
|
240
|
+
"simple-memory/package.json": '{"name":"simple-memory"}',
|
|
241
|
+
}),
|
|
242
|
+
workspacePluginsDir: pluginsDir,
|
|
243
|
+
},
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// THEN a hidden manifest records the resolved coordinates (no commit for
|
|
247
|
+
// first-party) and is not counted as a plugin file
|
|
248
|
+
const manifest = JSON.parse(
|
|
249
|
+
readFileSync(join(target, ".vellum-plugin.json"), "utf-8"),
|
|
250
|
+
);
|
|
251
|
+
expect(manifest.name).toBe("simple-memory");
|
|
252
|
+
expect(manifest.source.kind).toBe("first-party");
|
|
253
|
+
expect(manifest.source.repo).toBe("vellum-assistant");
|
|
254
|
+
expect(manifest.source.ref).toBe("main");
|
|
255
|
+
expect(manifest.commit).toBeUndefined();
|
|
256
|
+
});
|
|
257
|
+
|
|
161
258
|
test("refuses to overwrite an existing install without --force", async () => {
|
|
162
259
|
const target = join(pluginsDir, "simple-memory");
|
|
163
260
|
mkdirSync(target, { recursive: true });
|
|
@@ -168,7 +265,6 @@ describe("installPlugin", () => {
|
|
|
168
265
|
{ name: "simple-memory", force: false, ref: "main" },
|
|
169
266
|
{
|
|
170
267
|
fetch: fixtureFetch({
|
|
171
|
-
"simple-memory": null,
|
|
172
268
|
"simple-memory/package.json": "{}",
|
|
173
269
|
}),
|
|
174
270
|
workspacePluginsDir: pluginsDir,
|
|
@@ -189,7 +285,6 @@ describe("installPlugin", () => {
|
|
|
189
285
|
{ name: "simple-memory", force: true, ref: "main" },
|
|
190
286
|
{
|
|
191
287
|
fetch: fixtureFetch({
|
|
192
|
-
"simple-memory": null,
|
|
193
288
|
"simple-memory/package.json": '{"name":"simple-memory"}',
|
|
194
289
|
}),
|
|
195
290
|
workspacePluginsDir: pluginsDir,
|
|
@@ -201,9 +296,9 @@ describe("installPlugin", () => {
|
|
|
201
296
|
});
|
|
202
297
|
|
|
203
298
|
test("--force preserves the existing install when the fetch fails", async () => {
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
299
|
+
// A transient 5xx during a forced re-install must NOT delete the
|
|
300
|
+
// previously working plugin. The fetch error surfaces, but the existing
|
|
301
|
+
// tree on disk is untouched (all writes go through the staging dir).
|
|
207
302
|
const target = join(pluginsDir, "simple-memory");
|
|
208
303
|
mkdirSync(target, { recursive: true });
|
|
209
304
|
writeFileSync(join(target, "marker"), "pre-existing");
|
|
@@ -219,12 +314,9 @@ describe("installPlugin", () => {
|
|
|
219
314
|
),
|
|
220
315
|
).rejects.toThrow(/HTTP 503/);
|
|
221
316
|
|
|
222
|
-
// Marker is still there because the failed install never touched the
|
|
223
|
-
// target — the staging dir handles all writes until the swap.
|
|
224
317
|
expect(readFileSync(join(target, "marker"), "utf-8")).toBe("pre-existing");
|
|
225
318
|
// And no staging dir leaks into the plugins directory.
|
|
226
|
-
|
|
227
|
-
expect(remaining).toEqual(["simple-memory"]);
|
|
319
|
+
expect(readdirSync(pluginsDir)).toEqual(["simple-memory"]);
|
|
228
320
|
});
|
|
229
321
|
|
|
230
322
|
test("404 on the canonical path is reported as not-found", async () => {
|
|
@@ -259,63 +351,109 @@ describe("installPlugin", () => {
|
|
|
259
351
|
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
260
352
|
});
|
|
261
353
|
|
|
262
|
-
test("
|
|
263
|
-
|
|
354
|
+
test("a rate-limited contents listing surfaces a retryable PluginSourceUnavailableError", async () => {
|
|
355
|
+
// GIVEN GitHub's unauthenticated rate limit is exhausted: the contents
|
|
356
|
+
// listing 403s with the remaining-quota header at zero
|
|
357
|
+
const rateLimited: FetchLike = (async (input: RequestInfo | URL) => {
|
|
358
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
359
|
+
if (url.includes("marketplace.json")) {
|
|
360
|
+
return new Response("not found", { status: 404 });
|
|
361
|
+
}
|
|
362
|
+
if (url.includes("/contents/")) {
|
|
363
|
+
return new Response("rate limited", {
|
|
364
|
+
status: 403,
|
|
365
|
+
headers: { "x-ratelimit-remaining": "0" },
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return new Response("not found", { status: 404 });
|
|
369
|
+
}) as FetchLike;
|
|
370
|
+
|
|
371
|
+
// WHEN we install
|
|
372
|
+
// THEN the failure is classified as transient (retryable), not a hard
|
|
373
|
+
// error, so the route can surface a 503 instead of a 500
|
|
374
|
+
await expect(
|
|
375
|
+
installPlugin(
|
|
376
|
+
{ name: "demo", force: false, ref: "main" },
|
|
377
|
+
{ fetch: rateLimited, workspacePluginsDir: pluginsDir },
|
|
378
|
+
),
|
|
379
|
+
).rejects.toBeInstanceOf(PluginSourceUnavailableError);
|
|
380
|
+
|
|
381
|
+
// AND no staging dir leaks behind on the transient failure.
|
|
382
|
+
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("a forbidden contents listing with quota remaining stays a hard error", async () => {
|
|
386
|
+
// GIVEN a 403 that is NOT a rate-limit (quota header present and nonzero):
|
|
387
|
+
// a genuine authorization failure, not a transient one
|
|
388
|
+
const forbidden: FetchLike = (async (input: RequestInfo | URL) => {
|
|
389
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
390
|
+
if (url.includes("marketplace.json")) {
|
|
391
|
+
return new Response("not found", { status: 404 });
|
|
392
|
+
}
|
|
393
|
+
if (url.includes("/contents/")) {
|
|
394
|
+
return new Response("forbidden", {
|
|
395
|
+
status: 403,
|
|
396
|
+
headers: { "x-ratelimit-remaining": "57" },
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
return new Response("not found", { status: 404 });
|
|
400
|
+
}) as FetchLike;
|
|
401
|
+
|
|
402
|
+
// WHEN we install
|
|
403
|
+
// THEN it surfaces as a hard error — NOT the retryable variant — so the
|
|
404
|
+
// route maps it to 500 rather than inviting an endless retry loop
|
|
405
|
+
await expect(
|
|
406
|
+
installPlugin(
|
|
407
|
+
{ name: "demo", force: false, ref: "main" },
|
|
408
|
+
{ fetch: forbidden, workspacePluginsDir: pluginsDir },
|
|
409
|
+
),
|
|
410
|
+
).rejects.not.toBeInstanceOf(PluginSourceUnavailableError);
|
|
411
|
+
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("respects ref by forwarding it to the contents listing", async () => {
|
|
415
|
+
// The requested ref must reach the first-party contents listing request.
|
|
416
|
+
let listRef: string | undefined;
|
|
417
|
+
const base = fixtureFetch({ "demo/package.json": "{}" });
|
|
418
|
+
const fetch: FetchLike = async (input, init) => {
|
|
419
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
420
|
+
const match = /\/contents\/experimental\/plugins\/demo\?ref=([^&]+)/.exec(
|
|
421
|
+
url,
|
|
422
|
+
);
|
|
423
|
+
if (match) listRef = decodeURIComponent(match[1]!);
|
|
424
|
+
return base(url, init);
|
|
425
|
+
};
|
|
426
|
+
|
|
264
427
|
await installPlugin(
|
|
265
428
|
{ name: "demo", force: false, ref: "feat-branch" },
|
|
266
|
-
{
|
|
267
|
-
fetch: (async (input: RequestInfo | URL) => {
|
|
268
|
-
const url = typeof input === "string" ? input : input.toString();
|
|
269
|
-
if (url.includes("api.github.com")) {
|
|
270
|
-
const m = /[?&]ref=([^&]+)/.exec(url);
|
|
271
|
-
seenRef = m ? decodeURIComponent(m[1]!) : undefined;
|
|
272
|
-
return new Response(
|
|
273
|
-
JSON.stringify([
|
|
274
|
-
{
|
|
275
|
-
name: "package.json",
|
|
276
|
-
path: "experimental/plugins/demo/package.json",
|
|
277
|
-
type: "file",
|
|
278
|
-
size: 2,
|
|
279
|
-
download_url:
|
|
280
|
-
"https://raw.githubusercontent.com/vellum-ai/vellum-assistant/feat-branch/experimental/plugins/demo/package.json",
|
|
281
|
-
},
|
|
282
|
-
]),
|
|
283
|
-
{
|
|
284
|
-
status: 200,
|
|
285
|
-
headers: { "content-type": "application/json" },
|
|
286
|
-
},
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
return new Response("{}", { status: 200 });
|
|
290
|
-
}) as FetchLike,
|
|
291
|
-
workspacePluginsDir: pluginsDir,
|
|
292
|
-
},
|
|
429
|
+
{ fetch, workspacePluginsDir: pluginsDir },
|
|
293
430
|
);
|
|
294
431
|
|
|
295
|
-
expect(
|
|
432
|
+
expect(listRef).toBe("feat-branch");
|
|
296
433
|
expect(existsSync(join(pluginsDir, "demo", "package.json"))).toBe(true);
|
|
297
434
|
});
|
|
298
435
|
|
|
299
436
|
test("rejects untrusted entry names from the GitHub response", async () => {
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
437
|
+
// Even though GitHub returns trustworthy data, defense-in-depth requires
|
|
438
|
+
// us to validate every entry name before any filesystem write. A malicious
|
|
439
|
+
// or buggy upstream that hands us `../escape` must not write outside the
|
|
440
|
+
// target.
|
|
304
441
|
const badFetch: FetchLike = (async (input: RequestInfo | URL) => {
|
|
305
442
|
const url = typeof input === "string" ? input : input.toString();
|
|
306
|
-
if (url.includes("
|
|
443
|
+
if (url.includes("marketplace.json")) {
|
|
444
|
+
return new Response("not found", { status: 404 });
|
|
445
|
+
}
|
|
446
|
+
if (url.includes("/contents/")) {
|
|
307
447
|
return new Response(
|
|
308
448
|
JSON.stringify([
|
|
309
449
|
{
|
|
310
450
|
name: "../escape",
|
|
311
451
|
path: "experimental/plugins/demo/../escape",
|
|
312
452
|
type: "file",
|
|
313
|
-
|
|
314
|
-
download_url:
|
|
315
|
-
"https://raw.githubusercontent.com/vellum-ai/vellum-assistant/main/experimental/plugins/demo/escape",
|
|
453
|
+
download_url: `${DOWNLOAD_HOST}escape`,
|
|
316
454
|
},
|
|
317
455
|
]),
|
|
318
|
-
{ status: 200
|
|
456
|
+
{ status: 200 },
|
|
319
457
|
);
|
|
320
458
|
}
|
|
321
459
|
return new Response("x", { status: 200 });
|
|
@@ -335,6 +473,186 @@ describe("installPlugin", () => {
|
|
|
335
473
|
});
|
|
336
474
|
});
|
|
337
475
|
|
|
476
|
+
describe("installPlugin — marketplace resolution", () => {
|
|
477
|
+
let ws: string;
|
|
478
|
+
let pluginsDir: string;
|
|
479
|
+
|
|
480
|
+
beforeEach(() => {
|
|
481
|
+
ws = mkdtempSync(join(tmpdir(), "vellum-plugins-mkt-"));
|
|
482
|
+
pluginsDir = join(ws, "plugins");
|
|
483
|
+
mkdirSync(pluginsDir, { recursive: true });
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
afterEach(() => {
|
|
487
|
+
rmSync(ws, { recursive: true, force: true });
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const CAVEMAN_MANIFEST = {
|
|
491
|
+
name: "vellum-assistant",
|
|
492
|
+
plugins: [
|
|
493
|
+
{
|
|
494
|
+
name: "caveman",
|
|
495
|
+
source: {
|
|
496
|
+
source: "github",
|
|
497
|
+
repo: "JuliusBrussee/caveman",
|
|
498
|
+
ref: "v1.8.2",
|
|
499
|
+
},
|
|
500
|
+
description: "Ultra-compressed communication mode.",
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
test("installs a whitelisted plugin by shallow-cloning its pinned repo + ref", async () => {
|
|
506
|
+
// GIVEN a marketplace whitelisting caveman at its repo root, pinned to a tag
|
|
507
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
508
|
+
const calls: string[][] = [];
|
|
509
|
+
const runGit = fakeGitRunner({
|
|
510
|
+
tree: {
|
|
511
|
+
"package.json": '{"name":"caveman"}',
|
|
512
|
+
"README.md": "# caveman",
|
|
513
|
+
".claude-plugin": null,
|
|
514
|
+
".claude-plugin/plugin.json": "{}",
|
|
515
|
+
},
|
|
516
|
+
commit: "1111111222222233333334444444555555666666",
|
|
517
|
+
calls,
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
// WHEN we install by name (the install ref is ignored in favor of the
|
|
521
|
+
// manifest's pinned ref)
|
|
522
|
+
const result = await installPlugin(
|
|
523
|
+
{ name: "caveman", ref: "main" },
|
|
524
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// THEN the external tree is materialized under <pluginsDir>/caveman, the
|
|
528
|
+
// result reports the pinned ref and resolved commit, and `.git` is dropped
|
|
529
|
+
const target = join(pluginsDir, "caveman");
|
|
530
|
+
expect(result.target).toBe(target);
|
|
531
|
+
expect(result.ref).toBe("v1.8.2");
|
|
532
|
+
expect(result.commit).toBe("1111111222222233333334444444555555666666");
|
|
533
|
+
expect(result.fileCount).toBe(3);
|
|
534
|
+
expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
|
|
535
|
+
'{"name":"caveman"}',
|
|
536
|
+
);
|
|
537
|
+
expect(existsSync(join(target, ".claude-plugin", "plugin.json"))).toBe(
|
|
538
|
+
true,
|
|
539
|
+
);
|
|
540
|
+
expect(existsSync(join(target, ".git"))).toBe(false);
|
|
541
|
+
|
|
542
|
+
// AND the clone fetched the pinned ref from the pinned repo URL.
|
|
543
|
+
const fetchCall = calls.find((c) => c[0] === "fetch");
|
|
544
|
+
expect(fetchCall).toContain("v1.8.2");
|
|
545
|
+
const remoteCall = calls.find((c) => c[0] === "remote");
|
|
546
|
+
expect(remoteCall?.at(-1)).toBe(
|
|
547
|
+
"https://github.com/JuliusBrussee/caveman.git",
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
// AND a provenance manifest records the external source + commit.
|
|
551
|
+
const manifest = JSON.parse(
|
|
552
|
+
readFileSync(join(target, ".vellum-plugin.json"), "utf-8"),
|
|
553
|
+
);
|
|
554
|
+
expect(manifest.source.kind).toBe("external");
|
|
555
|
+
expect(manifest.source.owner).toBe("JuliusBrussee");
|
|
556
|
+
expect(manifest.source.ref).toBe("v1.8.2");
|
|
557
|
+
expect(manifest.commit).toBe("1111111222222233333334444444555555666666");
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
test("a missing remote ref surfaces a clean not-found", async () => {
|
|
561
|
+
// GIVEN a clone whose fetch fails because the ref doesn't exist
|
|
562
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
563
|
+
const runGit = fakeGitRunner({
|
|
564
|
+
tree: {},
|
|
565
|
+
fetchError: Object.assign(new Error("git fetch failed"), {
|
|
566
|
+
stderr: "fatal: couldn't find remote ref v1.8.2",
|
|
567
|
+
}),
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// WHEN we install
|
|
571
|
+
// THEN it is a hard not-found, not a retryable error
|
|
572
|
+
await expect(
|
|
573
|
+
installPlugin(
|
|
574
|
+
{ name: "caveman", ref: "main" },
|
|
575
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
576
|
+
),
|
|
577
|
+
).rejects.toBeInstanceOf(PluginNotFoundError);
|
|
578
|
+
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("a network failure during clone surfaces a retryable error", async () => {
|
|
582
|
+
// GIVEN a clone whose fetch fails with a transient network error
|
|
583
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
584
|
+
const runGit = fakeGitRunner({
|
|
585
|
+
tree: {},
|
|
586
|
+
fetchError: Object.assign(new Error("git fetch failed"), {
|
|
587
|
+
stderr: "fatal: unable to access: Could not resolve host: github.com",
|
|
588
|
+
}),
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// WHEN we install
|
|
592
|
+
// THEN it surfaces as the retryable variant so the route maps it to 503
|
|
593
|
+
await expect(
|
|
594
|
+
installPlugin(
|
|
595
|
+
{ name: "caveman", ref: "main" },
|
|
596
|
+
{ fetch, runGit, workspacePluginsDir: pluginsDir },
|
|
597
|
+
),
|
|
598
|
+
).rejects.toBeInstanceOf(PluginSourceUnavailableError);
|
|
599
|
+
expect(readdirSync(pluginsDir)).toEqual([]);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("a name absent from the manifest falls back to the first-party source", async () => {
|
|
603
|
+
// GIVEN a manifest that does NOT whitelist "simple-memory", and no such
|
|
604
|
+
// first-party tree in the canonical repo
|
|
605
|
+
const fetch = makeContentsFetch({ tree: {}, manifest: CAVEMAN_MANIFEST });
|
|
606
|
+
|
|
607
|
+
// WHEN we install a first-party name
|
|
608
|
+
// THEN resolution falls back to the first-party path and surfaces a clean
|
|
609
|
+
// not-found pointing at the first-party source (git is never invoked)
|
|
610
|
+
await expect(
|
|
611
|
+
installPlugin(
|
|
612
|
+
{ name: "simple-memory", ref: "main" },
|
|
613
|
+
{ fetch, runGit: unusedGitRunner, workspacePluginsDir: pluginsDir },
|
|
614
|
+
),
|
|
615
|
+
).rejects.toMatchObject({
|
|
616
|
+
constructor: PluginNotFoundError,
|
|
617
|
+
message: expect.stringContaining("vellum-ai/vellum-assistant"),
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
test("first-party plugin wins a name collision with the marketplace", async () => {
|
|
622
|
+
// GIVEN a manifest whitelisting "caveman" externally
|
|
623
|
+
// AND an in-repo first-party plugin that also claims the name "caveman"
|
|
624
|
+
const fetch = makeContentsFetch({
|
|
625
|
+
tree: {
|
|
626
|
+
"experimental/plugins/caveman/package.json":
|
|
627
|
+
'{"name":"@vellumai/caveman"}',
|
|
628
|
+
"experimental/plugins/caveman/hooks": null,
|
|
629
|
+
"experimental/plugins/caveman/hooks/init.ts": "// first-party",
|
|
630
|
+
},
|
|
631
|
+
manifest: CAVEMAN_MANIFEST,
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// WHEN we install by the colliding name (git must NOT run — the in-repo
|
|
635
|
+
// plugin is served entirely from the Contents API)
|
|
636
|
+
const result = await installPlugin(
|
|
637
|
+
{ name: "caveman", ref: "main" },
|
|
638
|
+
{ fetch, runGit: unusedGitRunner, workspacePluginsDir: pluginsDir },
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
// THEN the in-repo plugin is installed (matching what the search catalog
|
|
642
|
+
// advertises for the name), not the external repo — the marketplace is
|
|
643
|
+
// additive and never overrides a first-party plugin
|
|
644
|
+
const target = join(pluginsDir, "caveman");
|
|
645
|
+
expect(result.ref).toBe("main");
|
|
646
|
+
expect(result.commit).toBeNull();
|
|
647
|
+
expect(readFileSync(join(target, "package.json"), "utf-8")).toBe(
|
|
648
|
+
'{"name":"@vellumai/caveman"}',
|
|
649
|
+
);
|
|
650
|
+
expect(readFileSync(join(target, "hooks", "init.ts"), "utf-8")).toBe(
|
|
651
|
+
"// first-party",
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
338
656
|
describe("sanitizePluginName", () => {
|
|
339
657
|
test.each([
|
|
340
658
|
["../escape"],
|