@vellumai/assistant 0.11.3 → 0.11.4-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +11 -6
- package/docs/architecture/memory.md +11 -0
- package/docs/architecture/turn-actor.md +70 -0
- package/docs/flux-turn-detection-spike.md +243 -0
- package/docs/stt-provider-onboarding.md +3 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +34 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +11 -0
- package/openapi.yaml +139 -37
- package/package.json +1 -1
- package/scripts/voice-ttft-spike.ts +3 -3
- package/src/__tests__/app-compiler.test.ts +38 -3
- package/src/__tests__/attachments-store.test.ts +21 -12
- package/src/__tests__/byok-default-profile-ensure.test.ts +17 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +0 -1
- package/src/__tests__/call-site-routing-provider.test.ts +1 -1
- package/src/__tests__/channel-availability-routes.test.ts +14 -1
- package/src/__tests__/channel-capabilities-dedupe.test.ts +214 -0
- package/src/__tests__/channel-delivery-store.test.ts +14 -14
- package/src/__tests__/config-loader-backfill.test.ts +3 -3
- package/src/__tests__/config-schema.test.ts +25 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +8 -11
- package/src/__tests__/conversation-agent-loop.test.ts +28 -20
- package/src/__tests__/conversation-attention-store.test.ts +63 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -4
- package/src/__tests__/conversation-fork-crud.test.ts +69 -0
- package/src/__tests__/conversation-fork-referential.test.ts +67 -0
- package/src/__tests__/conversation-fork-retrospective.test.ts +24 -0
- package/src/__tests__/conversation-notifiers-provenance.test.ts +59 -0
- package/src/__tests__/conversation-queue.test.ts +177 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +134 -102
- package/src/__tests__/conversation-runtime-workspace.test.ts +14 -10
- package/src/__tests__/credential-prompt-route.test.ts +7 -10
- package/src/__tests__/custom-profile-ensure.test.ts +5 -1
- package/src/__tests__/discord-access-request-privacy.test.ts +5 -1
- package/src/__tests__/discord-requester-notice-privacy.test.ts +3 -3
- package/src/__tests__/document-append-idempotency.test.ts +233 -0
- package/src/__tests__/edit-propagation.test.ts +0 -7
- package/src/__tests__/helpers/mock-actor-context.ts +49 -0
- package/src/__tests__/helpers/mock-conversation.ts +13 -1
- package/src/__tests__/injector-chain.test.ts +63 -41
- package/src/__tests__/injector-disk-pressure.test.ts +11 -23
- package/src/__tests__/llm-context-resolution.test.ts +73 -1
- package/src/__tests__/llm-schema.test.ts +5 -2
- package/src/__tests__/mcp-list-plugin-servers.test.ts +250 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +6 -5
- package/src/__tests__/messages-read-boundary-guard.test.ts +134 -0
- package/src/__tests__/mtime-cache.test.ts +1 -1
- package/src/__tests__/non-member-access-request.test.ts +0 -20
- package/src/__tests__/outbound-slack-persistence.test.ts +40 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +5 -0
- package/src/__tests__/plugin-secret-pattern-contribution.test.ts +1 -1
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +14 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +20 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +434 -105
- package/src/__tests__/scoped-approval-grants.test.ts +11 -6
- package/src/__tests__/secret-ingress-channel.test.ts +0 -1
- package/src/__tests__/skills.test.ts +32 -0
- package/src/__tests__/slack-edit-ordering-characterization.test.ts +0 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +31 -19
- package/src/__tests__/subagent-spawn-and-await.test.ts +14 -10
- package/src/__tests__/turn-events-store.test.ts +43 -0
- package/src/__tests__/ui-shape-teaching.test.ts +33 -0
- package/src/__tests__/ui-voice-picker-surface.test.ts +128 -0
- package/src/__tests__/user-plugin-loader.test.ts +1 -1
- package/src/__tests__/visible-app-context.test.ts +16 -9
- package/src/__tests__/voice-config-update.test.ts +40 -0
- package/src/__tests__/worker-entrypoint-guards.test.ts +54 -0
- package/src/__tests__/worker-plugin-surface.test.ts +77 -0
- package/src/__tests__/workspace-migration-142-consolidate-voice-front-door.test.ts +158 -0
- package/src/__tests__/workspace-migration-143-repair-deprecated-codex-model-id.test.ts +133 -0
- package/src/__tests__/workspace-migration-144-convert-stranded-subscription-openai-profiles.test.ts +316 -0
- package/src/__tests__/workspace-migration-145-collapse-profile-bindings-to-entries.test.ts +325 -0
- package/src/__tests__/workspace-migration-146-repair-retired-fireworks-deepseek-flash-model-id.test.ts +235 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +10 -2
- package/src/acp/__tests__/auth-required.test.ts +161 -0
- package/src/acp/acp-claude-oauth.ts +19 -2
- package/src/acp/agent-process.test.ts +100 -0
- package/src/acp/agent-process.ts +29 -26
- package/src/acp/auth-required.ts +102 -0
- package/src/acp/session-manager.test.ts +119 -0
- package/src/acp/session-manager.ts +68 -2
- package/src/api/events/acp-auth-required.ts +55 -0
- package/src/api/index.ts +7 -0
- package/src/api/surfaces.ts +7 -3
- package/src/apps/app-store.ts +3 -0
- package/src/bundler/package-resolver.ts +2 -30
- package/src/calls/__tests__/voice-session-bridge.test.ts +173 -1
- package/src/calls/__tests__/voice-triage-escalate.test.ts +94 -0
- package/src/calls/call-controller.ts +19 -3
- package/src/calls/call-setup-flow.ts +0 -1
- package/src/calls/media-stream-stt-session.ts +15 -0
- package/src/calls/voice-session-bridge.ts +71 -16
- package/src/calls/voice-triage-escalate.ts +104 -2
- package/src/channels/__tests__/plugin-channel-declarations.test.ts +161 -0
- package/src/channels/config.ts +13 -0
- package/src/channels/plugin-channel-declarations.ts +108 -0
- package/src/channels/types.ts +30 -0
- package/src/cli/AGENTS.md +5 -2
- package/src/cli/commands/credentials.help.ts +2 -2
- package/src/cli/commands/inference-providers.ts +1 -1
- package/src/cli/commands/mcp.help.ts +13 -4
- package/src/cli/commands/mcp.ts +9 -0
- package/src/cli/commands/memory/__tests__/memory-v3.test.ts +128 -5
- package/src/cli/commands/memory/index.help.ts +43 -1
- package/src/cli/commands/memory/memory-v3.ts +64 -0
- package/src/cli/commands/stt.help.ts +27 -2
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +39 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/upgrade-plugin.ts +42 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +34 -2
- package/src/config/__tests__/default-provider.test.ts +6 -1
- package/src/config/__tests__/profile-materialization.test.ts +75 -19
- package/src/config/bundled-skills/acp/SKILL.md +6 -7
- package/src/config/bundled-skills/document-editor/SKILL.md +2 -2
- package/src/config/bundled-skills/document-editor/TOOLS.json +2 -2
- package/src/config/bundled-skills/media-processing/services/preprocess.ts +14 -4
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.test.ts +65 -0
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +7 -1
- package/src/config/bundled-skills/settings/tools/shared.ts +16 -0
- package/src/config/bundled-skills/settings/tools/voice-config-update.ts +19 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.test.ts +22 -1
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +9 -2
- package/src/config/call-site-defaults.ts +4 -5
- package/src/config/default-profile-catalog.ts +83 -12
- package/src/config/default-profile-names.ts +4 -1
- package/src/config/default-provider-resolution.ts +4 -0
- package/src/config/llm-context-resolution.ts +11 -3
- package/src/config/llm-resolver.ts +28 -1
- package/src/config/profile-materialization.ts +70 -22
- package/src/config/schemas/__tests__/live-voice.test.ts +107 -4
- package/src/config/schemas/call-site-catalog.ts +4 -4
- package/src/config/schemas/live-voice.ts +57 -23
- package/src/config/schemas/llm.ts +59 -32
- package/src/config/schemas/mcp.ts +23 -0
- package/src/config/schemas/plugin-updates.ts +6 -2
- package/src/config/schemas/stt.ts +1 -0
- package/src/context/outbound-sanitize.ts +96 -1
- package/src/daemon/__tests__/plugin-mcp-reconcile.test.ts +82 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +15 -10
- package/src/daemon/conversation-agent-loop.ts +17 -6
- package/src/daemon/conversation-messaging.ts +5 -1
- package/src/daemon/conversation-notifiers.ts +9 -1
- package/src/daemon/conversation-process.ts +36 -6
- package/src/daemon/conversation-runtime-assembly.ts +3 -4
- package/src/daemon/conversation-surfaces.ts +27 -5
- package/src/daemon/conversation-tool-setup.ts +1 -2
- package/src/daemon/conversation.ts +48 -0
- package/src/daemon/interactive-turn-sender.ts +59 -0
- package/src/daemon/mcp-reload-service.ts +36 -6
- package/src/daemon/process-message.ts +24 -24
- package/src/daemon/providers-setup.ts +6 -3
- package/src/daemon/trust-context-types.ts +29 -0
- package/src/daemon/wake-conversation-ops.ts +3 -2
- package/src/documents/document-store.ts +138 -5
- package/src/hooks/hook-loader.ts +3 -3
- package/src/hooks/registry.ts +50 -6
- package/src/inbound/__tests__/oauth-callback-url.test.ts +83 -0
- package/src/inbound/oauth-callback-url.ts +61 -0
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +1 -104
- package/src/live-voice/__tests__/live-voice-events.test.ts +7 -8
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +932 -0
- package/src/live-voice/__tests__/live-voice-metrics.test.ts +115 -8
- package/src/live-voice/__tests__/live-voice-photo.test.ts +100 -0
- package/src/live-voice/__tests__/live-voice-progress.test.ts +60 -194
- package/src/live-voice/__tests__/live-voice-stt.test.ts +14 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +29 -0
- package/src/live-voice/__tests__/live-voice-tts-session.test.ts +0 -483
- package/src/live-voice/__tests__/live-voice-vad.test.ts +0 -16
- package/src/live-voice/__tests__/progress-narration.test.ts +214 -0
- package/src/live-voice/live-voice-archive.ts +2 -0
- package/src/live-voice/live-voice-metrics.ts +57 -32
- package/src/live-voice/live-voice-photo.ts +1 -2
- package/src/live-voice/live-voice-session.ts +535 -314
- package/src/live-voice/progress-narration.ts +277 -0
- package/src/live-voice/protocol.ts +21 -1
- package/src/mcp/__tests__/effective-config.test.ts +238 -0
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/__tests__/mcp-oauth-client-registration.test.ts +200 -0
- package/src/mcp/__tests__/mcp-oauth-provider.test.ts +9 -9
- package/src/mcp/__tests__/plugin-server-credential-isolation.test.ts +95 -0
- package/src/mcp/client.ts +16 -11
- package/src/mcp/effective-config.ts +113 -0
- package/src/mcp/manager.ts +11 -6
- package/src/mcp/mcp-auth-orchestrator.ts +13 -22
- package/src/mcp/mcp-oauth-provider.ts +205 -240
- package/src/monitoring/__tests__/plugin-auto-update.test.ts +166 -3
- package/src/monitoring/plugin-auto-update.ts +128 -24
- package/src/notifications/signal.ts +1 -0
- package/src/permissions/confirmation-guardian-request.test.ts +15 -11
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/question-guardian-request.test.ts +14 -6
- package/src/permissions/question-guardian-request.ts +1 -2
- package/src/persistence/attachments-store.ts +8 -1
- package/src/persistence/bookmark-crud.ts +3 -7
- package/src/persistence/conversation-attention-store.ts +16 -45
- package/src/persistence/conversation-crud.ts +33 -4
- package/src/persistence/conversation-lineage.ts +9 -0
- package/src/persistence/conversation-queries.ts +108 -41
- package/src/persistence/delivery-crud.ts +38 -29
- package/src/persistence/external-conversation-store.ts +32 -4
- package/src/persistence/llm-request-log-store.ts +4 -10
- package/src/persistence/llm-usage-store.ts +8 -3
- package/src/persistence/message-reads.test.ts +197 -0
- package/src/persistence/message-reads.ts +211 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.test.ts +120 -0
- package/src/persistence/migrations/366-chatgpt-subscription-row-identity.ts +62 -0
- package/src/persistence/real-user-turn-filter.ts +27 -3
- package/src/persistence/steps.ts +9 -0
- package/src/plugin-api/__tests__/oauth-callback-url-export.test.ts +29 -0
- package/src/plugin-api/conversation-turn.ts +168 -5
- package/src/plugin-api/index.ts +21 -5
- package/src/plugin-api/vision-support.test.ts +1 -1
- package/src/plugins/__tests__/mcp-servers.test.ts +371 -0
- package/src/plugins/defaults/memory/AGENTS.md +4 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +204 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +72 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +4 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +4 -1
- package/src/plugins/defaults/memory/buffer-format.ts +165 -0
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +6 -0
- package/src/plugins/defaults/memory/graph/image-ref-utils.ts +3 -0
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -30
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.test.ts +34 -0
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +8 -12
- package/src/plugins/defaults/memory/hooks/post-compact.ts +1 -4
- package/src/plugins/defaults/memory/indexer.ts +3 -1
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +19 -7
- package/src/plugins/defaults/memory/src/__tests__/memory-v3-gate-stats.test.ts +281 -0
- package/src/plugins/defaults/memory/src/memory-v3-routes.ts +207 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +33 -0
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +199 -2
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +25 -18
- package/src/plugins/defaults/memory/substrate/skill-content.ts +8 -1
- package/src/plugins/defaults/memory/substrate/static-context.ts +160 -4
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +2 -4
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +3 -1
- package/src/plugins/defaults/memory/v3/prune.ts +2 -0
- package/src/plugins/defaults/memory/v3/selection-log-store.ts +2 -0
- package/src/plugins/defaults/memory/worker.ts +6 -3
- package/src/plugins/external-plugin-loader.ts +47 -0
- package/src/plugins/mcp-servers.ts +361 -0
- package/src/plugins/mtime-cache.ts +23 -49
- package/src/plugins/worker-plugin-surface.ts +33 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +1 -1
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +214 -2
- package/src/providers/__tests__/preflight-resolved-config.test.ts +57 -0
- package/src/providers/__tests__/retry-callsite.test.ts +5 -2
- package/src/providers/call-site-routing.ts +30 -3
- package/src/providers/connection-resolution.ts +194 -11
- package/src/providers/inference/auth.ts +6 -0
- package/src/providers/inference/connection-availability.ts +24 -2
- package/src/providers/inference/connections.ts +2 -0
- package/src/providers/model-catalog.ts +3 -3
- package/src/providers/model-intents.ts +28 -8
- package/src/providers/openai/chat-completions-provider.ts +5 -6
- package/src/providers/openai/codex-models.ts +2 -1
- package/src/providers/provider-send-message.ts +32 -3
- package/src/providers/speech-to-text/__tests__/deepgram-flux-frames.test.ts +433 -0
- package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +620 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +34 -0
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +285 -6
- package/src/providers/speech-to-text/deepgram-flux-frames.ts +395 -0
- package/src/providers/speech-to-text/deepgram-flux-realtime.ts +719 -0
- package/src/providers/speech-to-text/provider-catalog.ts +99 -8
- package/src/providers/speech-to-text/resolve.ts +25 -2
- package/src/routes/worker.ts +17 -5
- package/src/runtime/access-request-helper.ts +9 -12
- package/src/runtime/agent-wake.ts +3 -3
- package/src/runtime/pre-first-message-gate.ts +4 -0
- package/src/runtime/routes/__tests__/acp-claude-auth-routes.test.ts +12 -4
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +219 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +52 -0
- package/src/runtime/routes/__tests__/default-provider-routes.test.ts +61 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +102 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +44 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +25 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +62 -1
- package/src/runtime/routes/channel-availability-routes.ts +32 -14
- package/src/runtime/routes/channel-route-shared.ts +0 -6
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +6 -6
- package/src/runtime/routes/conversation-list-routes.ts +112 -1
- package/src/runtime/routes/conversation-query-routes.ts +40 -27
- package/src/runtime/routes/credential-prompt-routes.ts +4 -7
- package/src/runtime/routes/default-provider-routes.ts +15 -0
- package/src/runtime/routes/inbound-message-handler.ts +17 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -9
- package/src/runtime/routes/inbound-stages/admission-policy.ts +1 -17
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +2 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +1 -3
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +3 -4
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +0 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +11 -20
- package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +2 -3
- package/src/runtime/routes/inference-profiles-routes.ts +20 -11
- package/src/runtime/routes/inference-provider-connection-routes.ts +77 -15
- package/src/runtime/routes/log-export-routes.ts +3 -0
- package/src/runtime/routes/mcp-auth-routes.ts +148 -57
- package/src/runtime/routes/plugins-routes.ts +21 -3
- package/src/runtime/routes/stt-routes.ts +31 -25
- package/src/runtime/routes/surface-conversation-resolver.ts +3 -0
- package/src/runtime/routes/user-route-dispatcher.ts +39 -14
- package/src/runtime/routes/user-route-import.ts +108 -0
- package/src/schedule/worker.ts +6 -0
- package/src/security/oauth2.ts +6 -22
- package/src/stt/__tests__/daemon-batch-transcriber.test.ts +22 -0
- package/src/stt/__tests__/types.test.ts +94 -0
- package/src/stt/daemon-batch-transcriber.ts +10 -0
- package/src/stt/stt-stream-session.ts +8 -4
- package/src/stt/types.ts +103 -0
- package/src/subagent/manager.ts +1 -3
- package/src/subagent/types.ts +7 -6
- package/src/tools/acp/spawn.test.ts +97 -0
- package/src/tools/acp/spawn.ts +32 -0
- package/src/tools/document/document-tool.ts +12 -3
- package/src/tools/registry.ts +2 -1
- package/src/tools/ui-surface/surface-shape-docs.ts +11 -0
- package/src/tools/workflows/run-workflow.ts +1 -2
- package/src/tts/__tests__/reasoning-tag-filter.test.ts +78 -0
- package/src/tts/reasoning-tag-filter.ts +89 -0
- package/src/util/think-tag-stream.ts +95 -0
- package/src/workspace/byok-default-profile-ensure.ts +76 -24
- package/src/workspace/custom-profile-ensure.ts +4 -24
- package/src/workspace/migrations/142-consolidate-voice-front-door.ts +70 -0
- package/src/workspace/migrations/143-repair-deprecated-codex-model-id.ts +134 -0
- package/src/workspace/migrations/144-convert-stranded-subscription-openai-profiles.ts +265 -0
- package/src/workspace/migrations/145-collapse-profile-bindings-to-entries.ts +328 -0
- package/src/workspace/migrations/146-repair-retired-fireworks-deepseek-flash-model-id.ts +195 -0
- package/src/workspace/migrations/__tests__/141-stt-english-default-to-multilingual.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +10 -0
- package/src/workspace/provider-commit-message-generator.ts +7 -5
- package/src/live-voice/__tests__/front-decision.test.ts +0 -645
- package/src/live-voice/front-decision.ts +0 -476
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* stamp survives restarts;
|
|
11
11
|
* - only plugins that can actually move reach the daemon — up-to-date and
|
|
12
12
|
* disabled installs cost it nothing;
|
|
13
|
+
* - only curated marketplace installs are swept: a plugin installed straight
|
|
14
|
+
* from a GitHub URL, or one whose recorded source disagrees with the
|
|
15
|
+
* catalog entry claiming its name, is left for a human to upgrade, and
|
|
16
|
+
* every request the daemon does get carries `marketplaceOnly` so it
|
|
17
|
+
* enforces that boundary against its own inspection;
|
|
13
18
|
* - one plugin's refusal does not stop the sweep, an unreachable daemon
|
|
14
19
|
* abandons it without stamping (so it retries), and a timed-out upgrade
|
|
15
20
|
* abandons it *with* a stamp (so the retry cannot race the handler that
|
|
@@ -56,6 +61,25 @@ afterAll(() => {
|
|
|
56
61
|
let inspectStatus = new Map<string, string>();
|
|
57
62
|
let inspectThrowsFor = new Set<string>();
|
|
58
63
|
|
|
64
|
+
/** GitHub coordinates, as both the provenance sidecar and the catalog carry them. */
|
|
65
|
+
interface Coordinates {
|
|
66
|
+
/** `owner/repo`. */
|
|
67
|
+
readonly repo: string;
|
|
68
|
+
/** Repo-relative plugin root; absent = repo root. */
|
|
69
|
+
readonly path?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Provenance `inspectPlugin` reports per plugin name: where the installed copy
|
|
74
|
+
* says it came from, and where the catalog entry claiming that name points.
|
|
75
|
+
* Absent for a plugin that declares neither, which is what an older install
|
|
76
|
+
* with no sidecar looks like.
|
|
77
|
+
*/
|
|
78
|
+
let inspectProvenance = new Map<
|
|
79
|
+
string,
|
|
80
|
+
{ readonly source?: Coordinates; readonly marketplace?: Coordinates }
|
|
81
|
+
>();
|
|
82
|
+
|
|
59
83
|
type UpgradeReply = {
|
|
60
84
|
ok: boolean;
|
|
61
85
|
result?: { outcome?: string; toCommit?: string };
|
|
@@ -68,6 +92,8 @@ let upgradeReply: (name: string) => UpgradeReply = () => ({
|
|
|
68
92
|
result: { outcome: "upgraded", toCommit: "abc1234" },
|
|
69
93
|
});
|
|
70
94
|
const upgradeCalls: Array<{ name: string; strategy: unknown }> = [];
|
|
95
|
+
/** Full request bodies, for the assertions that care about more than strategy. */
|
|
96
|
+
const upgradeBodies: Array<Record<string, unknown>> = [];
|
|
71
97
|
|
|
72
98
|
const realInspect = await import("../../cli/lib/inspect-plugin.js");
|
|
73
99
|
const realIpcClient = await import("../../ipc/cli-client.js");
|
|
@@ -78,7 +104,20 @@ mock.module("../../cli/lib/inspect-plugin.js", () => ({
|
|
|
78
104
|
if (inspectThrowsFor.has(name)) {
|
|
79
105
|
throw new Error("marketplace unreachable");
|
|
80
106
|
}
|
|
81
|
-
|
|
107
|
+
const provenance = inspectProvenance.get(name);
|
|
108
|
+
const source = provenance?.source;
|
|
109
|
+
const marketplace = provenance?.marketplace;
|
|
110
|
+
const [owner = "", repo = ""] = source?.repo.split("/") ?? [];
|
|
111
|
+
return {
|
|
112
|
+
name,
|
|
113
|
+
status: inspectStatus.get(name) ?? "up-to-date",
|
|
114
|
+
local: source
|
|
115
|
+
? { source: { kind: "github", owner, repo, path: source.path } }
|
|
116
|
+
: null,
|
|
117
|
+
remote: marketplace
|
|
118
|
+
? { repo: marketplace.repo, path: marketplace.path ?? "" }
|
|
119
|
+
: null,
|
|
120
|
+
};
|
|
82
121
|
},
|
|
83
122
|
}));
|
|
84
123
|
|
|
@@ -86,12 +125,16 @@ mock.module("../../ipc/cli-client.js", () => ({
|
|
|
86
125
|
...realIpcClient,
|
|
87
126
|
cliIpcCall: async (
|
|
88
127
|
_method: string,
|
|
89
|
-
params: {
|
|
128
|
+
params: {
|
|
129
|
+
pathParams: { name: string };
|
|
130
|
+
body: Record<string, unknown>;
|
|
131
|
+
},
|
|
90
132
|
) => {
|
|
91
133
|
upgradeCalls.push({
|
|
92
134
|
name: params.pathParams.name,
|
|
93
135
|
strategy: params.body.strategy,
|
|
94
136
|
});
|
|
137
|
+
upgradeBodies.push(params.body);
|
|
95
138
|
return upgradeReply(params.pathParams.name);
|
|
96
139
|
},
|
|
97
140
|
}));
|
|
@@ -110,7 +153,14 @@ function writeConfig(pluginUpdates: Record<string, unknown>): void {
|
|
|
110
153
|
/** Materialize an installed plugin and declare what inspect says about it. */
|
|
111
154
|
function installPlugin(
|
|
112
155
|
name: string,
|
|
113
|
-
opts: {
|
|
156
|
+
opts: {
|
|
157
|
+
status?: string;
|
|
158
|
+
disabled?: boolean;
|
|
159
|
+
/** Coordinates the install's provenance sidecar records. */
|
|
160
|
+
source?: Coordinates;
|
|
161
|
+
/** Coordinates the catalog entry claiming this name points at. */
|
|
162
|
+
marketplace?: Coordinates;
|
|
163
|
+
} = {},
|
|
114
164
|
): void {
|
|
115
165
|
const dir = join(PLUGINS_DIR, name);
|
|
116
166
|
mkdirSync(dir, { recursive: true });
|
|
@@ -122,6 +172,12 @@ function installPlugin(
|
|
|
122
172
|
writeFileSync(join(dir, ".disabled"), "");
|
|
123
173
|
}
|
|
124
174
|
inspectStatus.set(name, opts.status ?? "update-available");
|
|
175
|
+
if (opts.source || opts.marketplace) {
|
|
176
|
+
inspectProvenance.set(name, {
|
|
177
|
+
source: opts.source,
|
|
178
|
+
marketplace: opts.marketplace,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
125
181
|
}
|
|
126
182
|
|
|
127
183
|
/** Backdate the stamp so the sweep is (or is not) due again. */
|
|
@@ -134,11 +190,13 @@ function stampAgedBy(ms: number): void {
|
|
|
134
190
|
beforeEach(() => {
|
|
135
191
|
inspectStatus = new Map();
|
|
136
192
|
inspectThrowsFor = new Set();
|
|
193
|
+
inspectProvenance = new Map();
|
|
137
194
|
upgradeReply = () => ({
|
|
138
195
|
ok: true,
|
|
139
196
|
result: { outcome: "upgraded", toCommit: "abc1234" },
|
|
140
197
|
});
|
|
141
198
|
upgradeCalls.length = 0;
|
|
199
|
+
upgradeBodies.length = 0;
|
|
142
200
|
rmSync(STAMP, { force: true });
|
|
143
201
|
rmSync(PLUGINS_DIR, { recursive: true, force: true });
|
|
144
202
|
mkdirSync(PLUGINS_DIR, { recursive: true });
|
|
@@ -203,6 +261,111 @@ describe("plugin auto-update sweep", () => {
|
|
|
203
261
|
expect(upgradeCalls.map((c) => c.name)).toEqual(["beta"]);
|
|
204
262
|
});
|
|
205
263
|
|
|
264
|
+
test("a plugin installed straight from a GitHub URL is never swept", async () => {
|
|
265
|
+
writeConfig({ mode: "auto" });
|
|
266
|
+
// No catalog entry claims the name, so its only upgrade target is whatever
|
|
267
|
+
// the recorded upstream ref points at right now. Nobody reviewed that.
|
|
268
|
+
installPlugin("alpha", { status: "not-in-marketplace" });
|
|
269
|
+
installPlugin("beta", { status: "update-available" });
|
|
270
|
+
|
|
271
|
+
const result = await runPluginAutoUpdateSweepIfDue();
|
|
272
|
+
|
|
273
|
+
expect(upgradeCalls.map((c) => c.name)).toEqual(["beta"]);
|
|
274
|
+
expect(result.skippedUntrusted).toEqual(["alpha"]);
|
|
275
|
+
expect(result.failed).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("every request asks the daemon to enforce the curated boundary itself", async () => {
|
|
279
|
+
writeConfig({ mode: "auto" });
|
|
280
|
+
installPlugin("alpha", { status: "update-available" });
|
|
281
|
+
installPlugin("beta", { status: "unknown-provenance" });
|
|
282
|
+
|
|
283
|
+
await runPluginAutoUpdateSweepIfDue();
|
|
284
|
+
|
|
285
|
+
// The daemon re-inspects before it moves anything, so a catalog entry that
|
|
286
|
+
// disappears after the inspection above would flip it to the direct path.
|
|
287
|
+
// `marketplaceOnly` makes the daemon refuse instead of following the
|
|
288
|
+
// install's mutable ref.
|
|
289
|
+
expect(upgradeBodies).toEqual([
|
|
290
|
+
{ strategy: "theirs", marketplaceOnly: true },
|
|
291
|
+
{ strategy: "theirs", marketplaceOnly: true },
|
|
292
|
+
]);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test("a whole workspace of untrusted installs stamps instead of retrying every minute", async () => {
|
|
296
|
+
writeConfig({ mode: "auto" });
|
|
297
|
+
installPlugin("alpha", { status: "not-in-marketplace" });
|
|
298
|
+
|
|
299
|
+
const result = await runPluginAutoUpdateSweepIfDue();
|
|
300
|
+
|
|
301
|
+
expect(result.skipped).toBe("no-candidates");
|
|
302
|
+
expect(result.skippedUntrusted).toEqual(["alpha"]);
|
|
303
|
+
expect(upgradeCalls).toEqual([]);
|
|
304
|
+
expect(await runPluginAutoUpdateSweepIfDue()).toMatchObject({
|
|
305
|
+
skipped: "not-due",
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("an install whose source disagrees with the catalog is left alone", async () => {
|
|
310
|
+
writeConfig({ mode: "auto" });
|
|
311
|
+
// A direct install sitting on a curated plugin's name: only the name lines
|
|
312
|
+
// up, so the sweep must not swap it for the catalog's code.
|
|
313
|
+
installPlugin("alpha", {
|
|
314
|
+
status: "update-available",
|
|
315
|
+
source: { repo: "someone-else/alpha" },
|
|
316
|
+
marketplace: { repo: "vellum-ai/alpha" },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const result = await runPluginAutoUpdateSweepIfDue();
|
|
320
|
+
|
|
321
|
+
expect(upgradeCalls).toEqual([]);
|
|
322
|
+
expect(result.skippedUntrusted).toEqual(["alpha"]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("an install from the catalog's own repo is upgraded", async () => {
|
|
326
|
+
writeConfig({ mode: "auto" });
|
|
327
|
+
installPlugin("alpha", {
|
|
328
|
+
status: "update-available",
|
|
329
|
+
source: { repo: "vellum-ai/plugins", path: "alpha" },
|
|
330
|
+
marketplace: { repo: "vellum-ai/plugins", path: "alpha" },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const result = await runPluginAutoUpdateSweepIfDue();
|
|
334
|
+
|
|
335
|
+
expect(upgradeCalls.map((c) => c.name)).toEqual(["alpha"]);
|
|
336
|
+
expect(result.skippedUntrusted).toEqual([]);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("a matching repo with a different plugin root is left alone", async () => {
|
|
340
|
+
writeConfig({ mode: "auto" });
|
|
341
|
+
// Same monorepo, different directory: not the plugin the catalog pins.
|
|
342
|
+
installPlugin("alpha", {
|
|
343
|
+
status: "update-available",
|
|
344
|
+
source: { repo: "vellum-ai/plugins", path: "experimental/alpha" },
|
|
345
|
+
marketplace: { repo: "vellum-ai/plugins", path: "alpha" },
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const result = await runPluginAutoUpdateSweepIfDue();
|
|
349
|
+
|
|
350
|
+
expect(upgradeCalls).toEqual([]);
|
|
351
|
+
expect(result.skippedUntrusted).toEqual(["alpha"]);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("an install with no recorded source is still re-pinned to the curated commit", async () => {
|
|
355
|
+
writeConfig({ mode: "auto" });
|
|
356
|
+
// An older or manually-copied copy: nothing about it contradicts the
|
|
357
|
+
// catalog, and the upgrade is what gives it provenance.
|
|
358
|
+
installPlugin("alpha", {
|
|
359
|
+
status: "unknown-provenance",
|
|
360
|
+
marketplace: { repo: "vellum-ai/alpha" },
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const result = await runPluginAutoUpdateSweepIfDue();
|
|
364
|
+
|
|
365
|
+
expect(upgradeCalls.map((c) => c.name)).toEqual(["alpha"]);
|
|
366
|
+
expect(result.upgraded).toEqual(["alpha"]);
|
|
367
|
+
});
|
|
368
|
+
|
|
206
369
|
test("a disabled plugin is left alone", async () => {
|
|
207
370
|
writeConfig({ mode: "auto" });
|
|
208
371
|
installPlugin("alpha");
|
|
@@ -8,6 +8,22 @@
|
|
|
8
8
|
* (default hourly), inspects which ones can actually move, and upgrades those
|
|
9
9
|
* with the configured merge strategy (default `theirs`).
|
|
10
10
|
*
|
|
11
|
+
* Only plugins that come from the curated marketplace are swept. The catalog
|
|
12
|
+
* pins every entry to an immutable commit that a curator reviewed, so an
|
|
13
|
+
* unattended upgrade can only land code that passed that review. A plugin
|
|
14
|
+
* installed straight from a GitHub URL has no such gate: it tracks a mutable
|
|
15
|
+
* ref (a branch, a tag, or the repo's default branch), so upgrading it means
|
|
16
|
+
* fetching and executing whatever upstream pushed since. That is a decision
|
|
17
|
+
* for a human at a terminal, not for an hourly background sweep, so untrusted
|
|
18
|
+
* installs are filtered out here and left for `assistant plugins upgrade`.
|
|
19
|
+
*
|
|
20
|
+
* The filter is applied twice, in both processes. Inspecting here keeps the
|
|
21
|
+
* daemon from being asked about plugins the sweep would never accept, but the
|
|
22
|
+
* daemon re-inspects before it moves anything, so this side's verdict cannot be
|
|
23
|
+
* the one that matters: a catalog entry that disappears in between would flip
|
|
24
|
+
* the daemon to the direct path. Every request therefore carries
|
|
25
|
+
* `marketplaceOnly`, which the daemon enforces against its own inspection.
|
|
26
|
+
*
|
|
11
27
|
* The monitor drives it — not the daemon — for the same reason it drives the
|
|
12
28
|
* plugin source watch and crash recovery: the work is periodic, network-bound,
|
|
13
29
|
* and must not compete with the daemon's turn loop. Drift detection happens
|
|
@@ -30,7 +46,10 @@
|
|
|
30
46
|
import { statSync, writeFileSync } from "node:fs";
|
|
31
47
|
import { join } from "node:path";
|
|
32
48
|
|
|
33
|
-
import {
|
|
49
|
+
import {
|
|
50
|
+
inspectPlugin,
|
|
51
|
+
type PluginInspection,
|
|
52
|
+
} from "../cli/lib/inspect-plugin.js";
|
|
34
53
|
import { listInstalledPlugins } from "../cli/lib/list-installed-plugins.js";
|
|
35
54
|
import { getConfigReadOnly } from "../config/loader.js";
|
|
36
55
|
import type { PluginUpdatesConfig } from "../config/schemas/plugin-updates.js";
|
|
@@ -67,22 +86,20 @@ const STAMP_FILENAME = "plugin-auto-update-last-run-at";
|
|
|
67
86
|
/**
|
|
68
87
|
* Inspection verdicts worth asking the daemon about.
|
|
69
88
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* have moved — only the daemon's upgrade can resolve that, and it no-ops when
|
|
76
|
-
* the ref still points at the installed commit.
|
|
89
|
+
* Both resolve to a curated marketplace pin, which is the only revision this
|
|
90
|
+
* sweep is willing to move an install to. `update-available` is the obvious
|
|
91
|
+
* one. `unknown-provenance` (an older or manually-copied install with no
|
|
92
|
+
* recorded commit) is included because an upgrade re-pins it to that same
|
|
93
|
+
* curated commit, which is how it stops being unknown.
|
|
77
94
|
*
|
|
78
|
-
* Everything else is skipped: `up-to-date` has nowhere to move,
|
|
79
|
-
* `remote-unavailable` means the catalog could not be read
|
|
80
|
-
*
|
|
95
|
+
* Everything else is skipped: `up-to-date` has nowhere to move,
|
|
96
|
+
* `remote-unavailable` means the catalog could not be read so an upgrade would
|
|
97
|
+
* fail on the same outage a moment later, and `not-in-marketplace` is the
|
|
98
|
+
* untrusted direct install whose only upgrade target is a mutable upstream ref.
|
|
81
99
|
*/
|
|
82
100
|
const UPGRADABLE_STATUSES: ReadonlySet<string> = new Set([
|
|
83
101
|
"update-available",
|
|
84
102
|
"unknown-provenance",
|
|
85
|
-
"not-in-marketplace",
|
|
86
103
|
]);
|
|
87
104
|
|
|
88
105
|
/** Outcome the daemon reports for one plugin; mirrors `PluginUpgradeResult`. */
|
|
@@ -113,31 +130,83 @@ function stampSweep(): void {
|
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Whether an installed copy actually tracks the curated source the sweep would
|
|
135
|
+
* upgrade it to.
|
|
136
|
+
*
|
|
137
|
+
* The provenance sidecar records the owner/repo/path the install was
|
|
138
|
+
* materialized from. When that names a different repository than the catalog
|
|
139
|
+
* entry claiming the plugin's name, the install is a direct (untrusted) one
|
|
140
|
+
* sitting on a curated name, and only its name lines up with the catalog. The
|
|
141
|
+
* sweep leaves it alone: the user picked that source, and swapping it for
|
|
142
|
+
* someone else's code is a call for a human running `assistant plugins
|
|
143
|
+
* upgrade`, not for an unattended hourly pass.
|
|
144
|
+
*
|
|
145
|
+
* An install with no recorded source at all (an older or manually-copied copy)
|
|
146
|
+
* is not disqualified. Nothing about it contradicts the catalog, and re-pinning
|
|
147
|
+
* it to the curated commit is exactly how it gains provenance.
|
|
148
|
+
*/
|
|
149
|
+
function tracksCuratedSource(inspection: PluginInspection): boolean {
|
|
150
|
+
const source = inspection.local?.source;
|
|
151
|
+
if (!source) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
const remote = inspection.remote;
|
|
155
|
+
if (!remote) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return (
|
|
159
|
+
`${source.owner}/${source.repo}`.toLowerCase() ===
|
|
160
|
+
remote.repo.toLowerCase() && (source.path ?? "") === remote.path
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** What one pass of inspection concluded about the installed plugins. */
|
|
165
|
+
interface UpgradeCandidates {
|
|
166
|
+
/** Names the daemon should be asked to upgrade. */
|
|
167
|
+
readonly candidates: readonly string[];
|
|
168
|
+
/** Names left alone because they do not come from the curated marketplace. */
|
|
169
|
+
readonly untrusted: readonly string[];
|
|
170
|
+
}
|
|
171
|
+
|
|
116
172
|
/**
|
|
117
173
|
* Installed plugins this sweep should ask the daemon to upgrade.
|
|
118
174
|
*
|
|
119
175
|
* Only user-installed plugins are listed (defaults ship with the assistant and
|
|
120
176
|
* have no upstream to advance to). A disabled plugin is deliberately left
|
|
121
|
-
* alone
|
|
177
|
+
* alone: the user switched it off, and upgrading it would re-materialize code
|
|
122
178
|
* and re-declare schedules for something that isn't running. What survives
|
|
123
179
|
* that is inspected, so the daemon is only asked about plugins that can
|
|
124
|
-
* actually move (see {@link UPGRADABLE_STATUSES})
|
|
180
|
+
* actually move (see {@link UPGRADABLE_STATUSES}) and whose move lands on a
|
|
181
|
+
* curated pin (see {@link tracksCuratedSource}).
|
|
125
182
|
*/
|
|
126
|
-
async function listUpgradableNames(): Promise<
|
|
183
|
+
async function listUpgradableNames(): Promise<UpgradeCandidates> {
|
|
127
184
|
const installed = listInstalledPlugins()
|
|
128
185
|
.map((plugin) => plugin.name)
|
|
129
186
|
.filter((name) => !isPluginDisabled(name));
|
|
130
187
|
|
|
131
188
|
const candidates: string[] = [];
|
|
189
|
+
const untrusted: string[] = [];
|
|
132
190
|
for (const name of installed) {
|
|
133
191
|
try {
|
|
134
192
|
const inspection = await inspectPlugin(
|
|
135
193
|
{ name },
|
|
136
194
|
{ fetch: globalThis.fetch.bind(globalThis) },
|
|
137
195
|
);
|
|
138
|
-
if (
|
|
139
|
-
|
|
196
|
+
if (inspection.status === "not-in-marketplace") {
|
|
197
|
+
// No catalog entry claims the name, so the only thing to upgrade to is
|
|
198
|
+
// whatever the recorded upstream ref points at right now.
|
|
199
|
+
untrusted.push(name);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!UPGRADABLE_STATUSES.has(inspection.status)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (!tracksCuratedSource(inspection)) {
|
|
206
|
+
untrusted.push(name);
|
|
207
|
+
continue;
|
|
140
208
|
}
|
|
209
|
+
candidates.push(name);
|
|
141
210
|
} catch (err) {
|
|
142
211
|
// An install whose drift cannot be classified (source unreachable,
|
|
143
212
|
// rate-limited) is skipped rather than handed to the daemon, which
|
|
@@ -145,10 +214,19 @@ async function listUpgradableNames(): Promise<string[]> {
|
|
|
145
214
|
log.debug({ err, name }, "Plugin auto-update could not inspect plugin");
|
|
146
215
|
}
|
|
147
216
|
}
|
|
148
|
-
return candidates;
|
|
217
|
+
return { candidates, untrusted };
|
|
149
218
|
}
|
|
150
219
|
|
|
151
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* Ask the daemon to upgrade one plugin.
|
|
222
|
+
*
|
|
223
|
+
* `marketplaceOnly` restates this sweep's boundary as something the daemon
|
|
224
|
+
* enforces rather than something it trusts the caller to have checked. The
|
|
225
|
+
* daemon re-inspects before it moves anything, so a catalog entry that
|
|
226
|
+
* disappears between the inspection above and the daemon's own would otherwise
|
|
227
|
+
* turn a curated upgrade into a direct one against the install's mutable ref.
|
|
228
|
+
* With the flag set the daemon refuses instead.
|
|
229
|
+
*/
|
|
152
230
|
function requestUpgrade(
|
|
153
231
|
name: string,
|
|
154
232
|
strategy: PluginUpdatesConfig["strategy"],
|
|
@@ -157,7 +235,7 @@ function requestUpgrade(
|
|
|
157
235
|
UPGRADE_IPC_METHOD,
|
|
158
236
|
// The target revision is never caller-supplied — the daemon resolves it
|
|
159
237
|
// from the plugin's own source, exactly as an interactive upgrade does.
|
|
160
|
-
{ pathParams: { name }, body: { strategy } },
|
|
238
|
+
{ pathParams: { name }, body: { strategy, marketplaceOnly: true } },
|
|
161
239
|
{ timeoutMs: UPGRADE_TIMEOUT_MS },
|
|
162
240
|
);
|
|
163
241
|
}
|
|
@@ -174,6 +252,12 @@ export interface PluginAutoUpdateSweepResult {
|
|
|
174
252
|
readonly upgraded: readonly string[];
|
|
175
253
|
readonly unchanged: readonly string[];
|
|
176
254
|
readonly failed: readonly string[];
|
|
255
|
+
/**
|
|
256
|
+
* Installs the sweep refused to touch because they do not come from the
|
|
257
|
+
* curated marketplace. They are not failures: a human can still upgrade them
|
|
258
|
+
* with `assistant plugins upgrade`.
|
|
259
|
+
*/
|
|
260
|
+
readonly skippedUntrusted: readonly string[];
|
|
177
261
|
/** True when the daemon could not be reached, so the sweep stays due. */
|
|
178
262
|
readonly daemonUnreachable: boolean;
|
|
179
263
|
}
|
|
@@ -182,6 +266,7 @@ const NOTHING: Omit<PluginAutoUpdateSweepResult, "skipped"> = {
|
|
|
182
266
|
upgraded: [],
|
|
183
267
|
unchanged: [],
|
|
184
268
|
failed: [],
|
|
269
|
+
skippedUntrusted: [],
|
|
185
270
|
daemonUnreachable: false,
|
|
186
271
|
};
|
|
187
272
|
|
|
@@ -214,18 +299,29 @@ export async function runPluginAutoUpdateSweepIfDue(): Promise<PluginAutoUpdateS
|
|
|
214
299
|
return { skipped: "not-due", ...NOTHING };
|
|
215
300
|
}
|
|
216
301
|
|
|
217
|
-
let
|
|
302
|
+
let inspected: UpgradeCandidates;
|
|
218
303
|
try {
|
|
219
|
-
|
|
304
|
+
inspected = await listUpgradableNames();
|
|
220
305
|
} catch (err) {
|
|
221
306
|
log.warn({ err }, "Plugin auto-update could not list installed plugins");
|
|
222
307
|
return { skipped: "no-candidates", ...NOTHING };
|
|
223
308
|
}
|
|
309
|
+
const { candidates: names, untrusted } = inspected;
|
|
310
|
+
if (untrusted.length > 0) {
|
|
311
|
+
log.info(
|
|
312
|
+
{ plugins: untrusted },
|
|
313
|
+
"Plugin auto-update skipped plugins that are not from the curated marketplace",
|
|
314
|
+
);
|
|
315
|
+
}
|
|
224
316
|
if (names.length === 0) {
|
|
225
317
|
// Stamp anyway: a workspace with nothing to move is a completed sweep, and
|
|
226
318
|
// re-inspecting every plugin a minute later would be pure churn.
|
|
227
319
|
stampSweep();
|
|
228
|
-
return {
|
|
320
|
+
return {
|
|
321
|
+
skipped: "no-candidates",
|
|
322
|
+
...NOTHING,
|
|
323
|
+
skippedUntrusted: untrusted,
|
|
324
|
+
};
|
|
229
325
|
}
|
|
230
326
|
|
|
231
327
|
const upgraded: string[] = [];
|
|
@@ -302,13 +398,21 @@ export async function runPluginAutoUpdateSweepIfDue(): Promise<PluginAutoUpdateS
|
|
|
302
398
|
upgraded: upgraded.length,
|
|
303
399
|
unchanged: unchanged.length,
|
|
304
400
|
failed: failed.length,
|
|
401
|
+
skippedUntrusted: untrusted.length,
|
|
305
402
|
strategy: config.strategy,
|
|
306
403
|
},
|
|
307
404
|
"Plugin auto-update sweep complete",
|
|
308
405
|
);
|
|
309
406
|
}
|
|
310
407
|
|
|
311
|
-
return {
|
|
408
|
+
return {
|
|
409
|
+
skipped: null,
|
|
410
|
+
upgraded,
|
|
411
|
+
unchanged,
|
|
412
|
+
failed,
|
|
413
|
+
skippedUntrusted: untrusted,
|
|
414
|
+
daemonUnreachable,
|
|
415
|
+
};
|
|
312
416
|
}
|
|
313
417
|
|
|
314
418
|
/** Handle for the running auto-update loop. */
|
|
@@ -26,6 +26,7 @@ export const NOTIFICATION_SOURCE_CHANNELS = [
|
|
|
26
26
|
{ id: "platform", description: "Platform-managed channel" },
|
|
27
27
|
{ id: "a2a", description: "Agent-to-agent protocol channel" },
|
|
28
28
|
{ id: "discord", description: "Discord channel" },
|
|
29
|
+
{ id: "plugin", description: "Channel brought by an installed plugin" },
|
|
29
30
|
{ id: "scheduler", description: "Scheduled task runner (reminders, cron)" },
|
|
30
31
|
{ id: "watcher", description: "File/event watcher subsystem" },
|
|
31
32
|
] as const;
|
|
@@ -19,17 +19,19 @@ mock.module("../util/logger.js", () => ({
|
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
21
|
mock.module("../daemon/conversation-registry.js", () => ({
|
|
22
|
-
findConversation: () =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
findConversation: () =>
|
|
23
|
+
asConversation({
|
|
24
|
+
assistantId: "self",
|
|
25
|
+
trustContext: {
|
|
26
|
+
trustClass: "trusted_contact",
|
|
27
|
+
sourceChannel: "telegram",
|
|
28
|
+
requesterExternalUserId: "tg-user-1",
|
|
29
|
+
requesterChatId: "tg-chat-1",
|
|
30
|
+
guardianExternalUserId: "tg-guardian-1",
|
|
31
|
+
guardianPrincipalId: "principal-1",
|
|
32
|
+
} satisfies TrustContext,
|
|
33
|
+
hasPendingConfirmation: () => confirmationPending,
|
|
34
|
+
}),
|
|
33
35
|
}));
|
|
34
36
|
|
|
35
37
|
mock.module("../channels/gateway-guardian-requests.js", () => ({
|
|
@@ -50,7 +52,9 @@ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
|
|
|
50
52
|
},
|
|
51
53
|
}));
|
|
52
54
|
|
|
55
|
+
import { asConversation } from "../__tests__/helpers/mock-conversation.js";
|
|
53
56
|
import type { AssistantEvent } from "../api/index.js";
|
|
57
|
+
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
54
58
|
import { createGuardianRequestForConfirmation } from "./confirmation-guardian-request.js";
|
|
55
59
|
|
|
56
60
|
const MSG = {
|
|
@@ -66,8 +66,8 @@ export async function createGuardianRequestForConfirmation(
|
|
|
66
66
|
|
|
67
67
|
const conversation = findConversation(conversationId);
|
|
68
68
|
const trustContext = opts?.preferTurnSnapshot
|
|
69
|
-
?
|
|
70
|
-
: conversation?.
|
|
69
|
+
? conversation?.getTurnOrRestingTrust()
|
|
70
|
+
: conversation?.getTrustContext();
|
|
71
71
|
const sourceChannel = trustContext?.sourceChannel ?? "vellum";
|
|
72
72
|
const inputRecord = msg.input as Record<string, unknown>;
|
|
73
73
|
const activityRaw =
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
9
|
|
|
10
|
+
import { mockTrustContext } from "../__tests__/helpers/mock-actor-context.js";
|
|
11
|
+
import { asConversation } from "../__tests__/helpers/mock-conversation.js";
|
|
10
12
|
import type { QuestionRequestEvent } from "../api/events/question-request.js";
|
|
13
|
+
import type { TrustContext } from "../daemon/trust-context-types.js";
|
|
11
14
|
|
|
12
15
|
const createGuardianRequestMock = mock(
|
|
13
16
|
(params: Record<string, unknown>): Promise<Record<string, unknown>> =>
|
|
@@ -21,13 +24,18 @@ const withdrawCardsMock = mock((_params: Record<string, unknown>) =>
|
|
|
21
24
|
Promise.resolve(),
|
|
22
25
|
);
|
|
23
26
|
|
|
24
|
-
let trustContext:
|
|
27
|
+
let trustContext: TrustContext | undefined;
|
|
25
28
|
let pendingInteraction: { kind: string } | undefined;
|
|
26
29
|
let rowAfterExpire: Record<string, unknown> | null = null;
|
|
27
30
|
|
|
28
31
|
mock.module("../daemon/conversation-registry.js", () => ({
|
|
29
32
|
findConversation: (_id: string) =>
|
|
30
|
-
trustContext
|
|
33
|
+
trustContext
|
|
34
|
+
? asConversation({
|
|
35
|
+
trustContext,
|
|
36
|
+
assistantId: "self",
|
|
37
|
+
})
|
|
38
|
+
: undefined,
|
|
31
39
|
}));
|
|
32
40
|
mock.module("../channels/gateway-guardian-requests.js", () => ({
|
|
33
41
|
createGuardianRequest: (params: Record<string, unknown>) =>
|
|
@@ -77,9 +85,9 @@ function makeEvent(
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
function guardianTrustContext(
|
|
80
|
-
overrides:
|
|
81
|
-
):
|
|
82
|
-
return {
|
|
88
|
+
overrides: Partial<TrustContext> = {},
|
|
89
|
+
): TrustContext {
|
|
90
|
+
return mockTrustContext({
|
|
83
91
|
trustClass: "guardian",
|
|
84
92
|
sourceChannel: "telegram",
|
|
85
93
|
guardianPrincipalId: "prin-guardian",
|
|
@@ -87,7 +95,7 @@ function guardianTrustContext(
|
|
|
87
95
|
requesterExternalUserId: "tg-guardian",
|
|
88
96
|
requesterChatId: "chat-1",
|
|
89
97
|
...overrides,
|
|
90
|
-
};
|
|
98
|
+
});
|
|
91
99
|
}
|
|
92
100
|
|
|
93
101
|
beforeEach(() => {
|
|
@@ -107,8 +107,7 @@ export async function createGuardianRequestForQuestion(
|
|
|
107
107
|
const conversation = findConversation(conversationId);
|
|
108
108
|
// The prompter runs inside the emitting turn — bind to the turn's trust
|
|
109
109
|
// snapshot so a concurrent context mutation can't repoint the request.
|
|
110
|
-
const trustContext =
|
|
111
|
-
conversation?.currentTurnTrustContext ?? conversation?.trustContext;
|
|
110
|
+
const trustContext = conversation?.getTurnOrRestingTrust();
|
|
112
111
|
if (!conversation || !trustContext) {
|
|
113
112
|
return;
|
|
114
113
|
}
|
|
@@ -137,6 +137,9 @@ function getAttachmentRow(attachmentId: string): AttachmentRow | null {
|
|
|
137
137
|
function getMessageConversationContext(
|
|
138
138
|
messageId: string,
|
|
139
139
|
): { conversationId: string; conversationCreatedAt: number } | null {
|
|
140
|
+
// Any-state read, deliberately: the id names a message the caller is
|
|
141
|
+
// already linking an attachment to, and a rendered row may still be
|
|
142
|
+
// streaming. Existence and ownership are the question, not completeness.
|
|
140
143
|
return (
|
|
141
144
|
rawGet<{ conversationId: string; conversationCreatedAt: number }>(
|
|
142
145
|
"attachments:getMessageConversationContext",
|
|
@@ -474,6 +477,11 @@ const ALLOWED_MIME_TYPES = new Set([
|
|
|
474
477
|
// Source code
|
|
475
478
|
"text/javascript",
|
|
476
479
|
"text/typescript",
|
|
480
|
+
// Shell scripts (browsers and OS file pickers report these for .sh files)
|
|
481
|
+
"application/x-sh",
|
|
482
|
+
"application/x-shellscript",
|
|
483
|
+
"text/x-sh",
|
|
484
|
+
"text/x-shellscript",
|
|
477
485
|
// Archives
|
|
478
486
|
"application/zip",
|
|
479
487
|
"application/gzip",
|
|
@@ -501,7 +509,6 @@ const ALLOWED_MIME_TYPES = new Set([
|
|
|
501
509
|
*/
|
|
502
510
|
const DANGEROUS_EXTENSIONS = new Set([
|
|
503
511
|
"exe",
|
|
504
|
-
"sh",
|
|
505
512
|
"bat",
|
|
506
513
|
"cmd",
|
|
507
514
|
"com",
|