@vellumai/assistant 0.10.2-dev.202606250318.5e7cfb0 → 0.10.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/bun.lock +0 -20
- package/docs/workspace-tools.md +33 -42
- package/eslint-rules/cli-no-daemon-internals.js +0 -6
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +0 -31
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +0 -44
- package/node_modules/@vellumai/gateway-client/src/index.ts +0 -14
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +0 -17
- package/node_modules/@vellumai/service-contracts/package.json +0 -1
- package/node_modules/@vellumai/service-contracts/src/index.ts +0 -1
- package/openapi.yaml +0 -155
- package/package.json +1 -4
- package/scripts/test.sh +15 -36
- package/src/__tests__/actor-token-service.test.ts +14 -36
- package/src/__tests__/agent-loop-override-profile.test.ts +0 -1
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -2
- package/src/__tests__/agent-wake-override-profile.test.ts +0 -2
- package/src/__tests__/annotate-activity-metadata.test.ts +0 -2
- package/src/__tests__/annotate-risk-options.test.ts +0 -2
- package/src/__tests__/approval-cascade.test.ts +0 -2
- package/src/__tests__/assistant-attachments.test.ts +0 -42
- package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
- package/src/__tests__/btw-routes.test.ts +0 -2
- package/src/__tests__/build-persisted-content.test.ts +0 -2
- package/src/__tests__/call-controller.test.ts +0 -19
- package/src/__tests__/channel-guardian.test.ts +58 -94
- package/src/__tests__/channel-reply-delivery.test.ts +0 -2
- package/src/__tests__/compaction-events.test.ts +0 -2
- package/src/__tests__/compaction.benchmark.test.ts +0 -2
- package/src/__tests__/compactor-call-site-logging.test.ts +0 -2
- package/src/__tests__/compactor-low-watermark-cut.test.ts +0 -2
- package/src/__tests__/compactor-preserved-tail-count.test.ts +0 -2
- package/src/__tests__/compactor-summary-call-truncation.test.ts +0 -2
- package/src/__tests__/compactor-web-search-strip.test.ts +0 -2
- package/src/__tests__/config-loader-backfill.test.ts +10 -123
- package/src/__tests__/config-schema.test.ts +0 -1
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +29 -31
- package/src/__tests__/contacts-relay-reads.test.ts +15 -13
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +0 -134
- package/src/__tests__/conversation-analysis-routes.test.ts +0 -2
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +0 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -2
- package/src/__tests__/conversation-history-web-search.test.ts +0 -2
- package/src/__tests__/conversation-load-history-repair.test.ts +0 -2
- package/src/__tests__/conversation-load-history-stripped.test.ts +0 -2
- package/src/__tests__/conversation-pairing.test.ts +0 -2
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +0 -2
- package/src/__tests__/conversation-process-callsite.test.ts +0 -2
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -2
- package/src/__tests__/conversation-queue.test.ts +0 -91
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +0 -14
- package/src/__tests__/conversation-routes-slash-commands.test.ts +0 -14
- package/src/__tests__/conversation-slash-queue.test.ts +0 -2
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -2
- package/src/__tests__/conversation-speed-override.test.ts +0 -2
- package/src/__tests__/conversation-surfaces-task-progress.test.ts +0 -29
- package/src/__tests__/conversation-title-service.test.ts +0 -2
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +0 -47
- package/src/__tests__/conversation-usage.test.ts +0 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -2
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -2
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -2
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/db-migration-rollback.test.ts +171 -205
- package/src/__tests__/db-test-helpers.ts +4 -5
- package/src/__tests__/deterministic-verification-control-plane.test.ts +2 -4
- package/src/__tests__/disk-pressure-guard.test.ts +0 -41
- package/src/__tests__/dm-persistence.test.ts +0 -2
- package/src/__tests__/emit-signal-routing-intent.test.ts +5 -10
- package/src/__tests__/events-dev-bypass-actor.test.ts +1 -7
- package/src/__tests__/exploration-drift-hook.test.ts +2 -3
- package/src/__tests__/filing-service.test.ts +0 -2
- package/src/__tests__/guardian-binding-drift-heal.test.ts +10 -75
- package/src/__tests__/guardian-dispatch.test.ts +1 -95
- package/src/__tests__/guardian-outbound-http.test.ts +0 -13
- package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -2
- package/src/__tests__/heartbeat-service.test.ts +0 -2
- package/src/__tests__/helpers/channel-test-adapter.ts +7 -1
- package/src/__tests__/host-app-control-routes.test.ts +30 -24
- package/src/__tests__/host-bash-routes.test.ts +41 -31
- package/src/__tests__/host-browser-routes.test.ts +32 -26
- package/src/__tests__/host-cu-routes-targeted.test.ts +33 -25
- package/src/__tests__/host-file-routes-targeted.test.ts +52 -40
- package/src/__tests__/host-transfer-routes-targeted.test.ts +43 -31
- package/src/__tests__/http-user-message-parity.test.ts +8 -290
- package/src/__tests__/inbound-invite-redemption.test.ts +0 -28
- package/src/__tests__/inbound-slack-persistence.test.ts +0 -2
- package/src/__tests__/invite-redemption-service.test.ts +0 -198
- package/src/__tests__/llm-context-normalization.test.ts +0 -105
- package/src/__tests__/llm-request-log-error-payload.test.ts +9 -71
- package/src/__tests__/llm-usage-store.test.ts +0 -25
- package/src/__tests__/mcp-health-check.test.ts +1 -2
- package/src/__tests__/media-stream-server-integration.test.ts +0 -127
- package/src/__tests__/memory-retrieval-hook.test.ts +0 -2
- package/src/__tests__/messaging-send-tool.test.ts +0 -2
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/__tests__/mtime-cache.test.ts +5 -146
- package/src/__tests__/native-web-search.test.ts +0 -2
- package/src/__tests__/non-member-access-request.test.ts +17 -189
- package/src/__tests__/notification-broadcaster.test.ts +0 -4
- package/src/__tests__/notification-decision-recipient-context.test.ts +32 -33
- package/src/__tests__/notification-deep-link.test.ts +0 -6
- package/src/__tests__/notification-guardian-path.test.ts +0 -19
- package/src/__tests__/openai-provider.test.ts +12 -22
- package/src/__tests__/openai-responses-provider.test.ts +2 -12
- package/src/__tests__/outbound-slack-persistence.test.ts +0 -2
- package/src/__tests__/pending-interactions-resolved-event.test.ts +4 -7
- package/src/__tests__/persistence-secret-redaction.test.ts +0 -2
- package/src/__tests__/plugin-bootstrap.test.ts +73 -3
- package/src/__tests__/plugin-route-contribution.test.ts +17 -4
- package/src/__tests__/plugin-tool-contribution.test.ts +18 -3
- package/src/__tests__/plugin-types.test.ts +2 -0
- package/src/__tests__/process-message-background-slack.test.ts +0 -2
- package/src/__tests__/process-message-display-content.test.ts +0 -2
- package/src/__tests__/provider-error-scenarios.test.ts +4 -5
- package/src/__tests__/provider-usage-tracking.test.ts +0 -39
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -2
- package/src/__tests__/registry.test.ts +1 -4
- package/src/__tests__/relay-server.test.ts +25 -694
- package/src/__tests__/runtime-attachment-metadata.test.ts +1 -0
- package/src/__tests__/secret-ingress-http.test.ts +0 -14
- package/src/__tests__/send-endpoint-busy.test.ts +8 -30
- package/src/__tests__/skills.test.ts +0 -44
- package/src/__tests__/slack-inbound-verification.test.ts +2 -47
- package/src/__tests__/stt-hints.test.ts +13 -44
- package/src/__tests__/subagent-detail.test.ts +0 -27
- package/src/__tests__/subagent-disposal.test.ts +0 -65
- package/src/__tests__/subagent-notify-parent.test.ts +0 -2
- package/src/__tests__/subagent-role-registry.test.ts +2 -7
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +0 -2
- package/src/__tests__/subagent-tools.test.ts +0 -2
- package/src/__tests__/suggestion-routes.test.ts +0 -2
- package/src/__tests__/title-generate-hook.test.ts +0 -2
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -2
- package/src/__tests__/tool-executor.test.ts +11 -16
- package/src/__tests__/tool-preview-lifecycle.test.ts +0 -2
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +0 -2
- package/src/__tests__/tool-start-timestamp.test.ts +0 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
- package/src/__tests__/twilio-routes.test.ts +0 -96
- package/src/__tests__/ui-file-upload-surface.test.ts +0 -86
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-invite-redemption.test.ts +0 -33
- package/src/__tests__/web-search-backend-failure.test.ts +0 -2
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +35 -14
- package/src/__tests__/workspace-tool-loader.test.ts +2 -195
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
- package/src/agent/loop.ts +0 -56
- package/src/api/index.ts +1 -19
- package/src/api/responses/llm-request-log-entry.ts +0 -29
- package/src/api/responses/subagent-detail.ts +0 -17
- package/src/api/surfaces.ts +3 -39
- package/src/approvals/guardian-request-resolvers.ts +11 -1
- package/src/calls/__tests__/relay-setup-router.test.ts +4 -262
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/guardian-dispatch.ts +8 -10
- package/src/calls/inbound-trust-reader.ts +1 -17
- package/src/calls/media-stream-server.ts +0 -21
- package/src/calls/relay-server.ts +50 -167
- package/src/calls/relay-setup-router.ts +7 -37
- package/src/calls/relay-verification.ts +4 -4
- package/src/calls/stt-hints.ts +12 -9
- package/src/calls/twilio-routes.ts +4 -14
- package/src/channels/types.ts +20 -10
- package/src/cli/commands/__tests__/cache.test.ts +1 -8
- package/src/cli/commands/cache.ts +181 -194
- package/src/cli/commands/db/__tests__/repair.test.ts +5 -6
- package/src/cli/commands/db/status.ts +1 -37
- package/src/cli/commands/mcp.ts +218 -252
- package/src/cli/commands/memory/index.ts +0 -2
- package/src/cli/commands/plugins.ts +3 -75
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -102
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +1 -160
- package/src/cli/lib/list-installed-plugins.ts +1 -179
- package/src/config/__tests__/sync-gated-profiles.test.ts +3 -11
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +17 -27
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +3 -13
- package/src/config/bundled-skills/subagent/SKILL.md +1 -1
- package/src/config/bundled-skills/subagent/TOOLS.json +1 -1
- package/src/config/feature-flag-registry.json +13 -5
- package/src/config/loader.ts +5 -38
- package/src/config/schemas/__tests__/memory-v3.test.ts +0 -1
- package/src/config/schemas/memory-lifecycle.ts +0 -12
- package/src/config/schemas/memory-v3.ts +0 -7
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/timeouts.ts +0 -8
- package/src/config/seed-inference-profiles.ts +11 -21
- package/src/config/skills.ts +5 -27
- package/src/config/sync-gated-profiles.ts +13 -12
- package/src/contacts/contacts-write.ts +0 -3
- package/src/daemon/assistant-attachments.ts +4 -27
- package/src/daemon/conversation-agent-loop.ts +0 -28
- package/src/daemon/conversation-process.ts +16 -35
- package/src/daemon/conversation-surfaces.ts +38 -111
- package/src/daemon/conversation-tool-setup.ts +16 -50
- package/src/daemon/conversation.ts +1 -13
- package/src/daemon/disk-pressure-guard.ts +2 -12
- package/src/daemon/event-loop-watchdog.ts +1 -28
- package/src/daemon/external-plugins-bootstrap.ts +34 -4
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -25
- package/src/daemon/handlers/config-a2a.ts +14 -6
- package/src/daemon/handlers/config-channels.ts +22 -78
- package/src/daemon/handlers/conversations.ts +0 -77
- package/src/daemon/lifecycle.ts +0 -4
- package/src/daemon/mcp-reload-service.ts +0 -10
- package/src/daemon/memory-v2-startup.test.ts +0 -72
- package/src/daemon/memory-v2-startup.ts +19 -87
- package/src/daemon/message-types/conversations.ts +0 -2
- package/src/daemon/message-types/surfaces.ts +12 -12
- package/src/daemon/server.ts +4 -0
- package/src/daemon/shutdown-handlers.ts +0 -20
- package/src/daemon/tool-setup-types.ts +0 -9
- package/src/daemon/workspace-tools-watcher.ts +328 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +2 -2
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +0 -1
- package/src/mcp/client.ts +1 -15
- package/src/mcp/mcp-auth-orchestrator.ts +1 -6
- package/src/mcp/mcp-oauth-provider.ts +8 -19
- package/src/memory/__tests__/memory-retrospective-job.test.ts +0 -8
- package/src/memory/conversation-crud.ts +0 -38
- package/src/memory/db-connection.ts +3 -22
- package/src/memory/db-init.ts +502 -36
- package/src/memory/db-singleton.ts +4 -6
- package/src/memory/jobs-worker.ts +0 -58
- package/src/memory/llm-request-log-store.ts +1 -26
- package/src/memory/llm-usage-store.ts +20 -48
- package/src/memory/memory-retrospective-job.ts +8 -9
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +56 -130
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/registry.ts +573 -0
- package/src/memory/migrations/run-migrations.ts +6 -90
- package/src/memory/migrations/validate-migration-state.ts +66 -101
- package/src/memory/schema/conversations.ts +0 -9
- package/src/memory/schema/infrastructure.ts +0 -20
- package/src/memory/v2/__tests__/cli-command-store.test.ts +0 -25
- package/src/memory/v2/__tests__/skill-store.test.ts +0 -80
- package/src/memory/v2/cli-command-store.ts +38 -75
- package/src/memory/v2/prompts/consolidation.ts +82 -13
- package/src/memory/v2/prompts/router.ts +93 -21
- package/src/memory/v2/skill-store.ts +31 -68
- package/src/notifications/__tests__/broadcaster.test.ts +8 -16
- package/src/notifications/__tests__/decision-engine.test.ts +9 -78
- package/src/notifications/broadcaster.ts +1 -8
- package/src/notifications/decision-engine.ts +7 -15
- package/src/notifications/destination-resolver.ts +24 -68
- package/src/notifications/emit-signal.ts +14 -39
- package/src/permissions/question-prompter.test.ts +1 -1
- package/src/permissions/question-prompter.ts +4 -7
- package/src/plugin-api/index.ts +6 -6
- package/src/plugin-api/types.ts +5 -3
- package/src/plugin-api/vision-support.test.ts +4 -28
- package/src/plugin-api/vision-support.ts +31 -66
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -161
- package/src/plugins/defaults/advisor/consult.ts +6 -110
- package/src/plugins/defaults/advisor/steering.ts +2 -14
- package/src/plugins/defaults/advisor/tools/advisor.ts +5 -32
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +1 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +7 -47
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +11 -10
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +20 -12
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +11 -42
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +3 -33
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +4 -48
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +8 -4
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +15 -43
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +2 -11
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -77
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +11 -12
- package/src/plugins/mtime-cache.ts +291 -76
- package/src/plugins/pipeline.ts +13 -111
- package/src/plugins/types.ts +2 -0
- package/src/providers/anthropic/client.ts +0 -5
- package/src/providers/call-site-routing.ts +0 -4
- package/src/providers/model-catalog.ts +0 -16
- package/src/providers/openai/__tests__/api-error-detail.test.ts +120 -0
- package/src/providers/openai/chat-completions-provider.ts +83 -37
- package/src/providers/openai/responses-provider.ts +46 -50
- package/src/providers/openrouter/client.ts +0 -5
- package/src/providers/provider-send-message.ts +0 -4
- package/src/providers/ratelimit.ts +0 -4
- package/src/providers/retry.ts +0 -4
- package/src/providers/types.ts +0 -9
- package/src/providers/usage-tracking.ts +0 -4
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +3 -335
- package/src/runtime/access-request-helper.ts +39 -19
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-stream-state.ts +2 -9
- package/src/runtime/auth/require-bound-guardian.ts +11 -21
- package/src/runtime/channel-verification-service.ts +31 -56
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
- package/src/runtime/guardian-vellum-migration.ts +7 -66
- package/src/runtime/invite-redemption-service.ts +187 -198
- package/src/runtime/local-actor-identity.ts +11 -76
- package/src/runtime/pending-interactions.ts +1 -11
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +5 -56
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +0 -187
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +5 -13
- package/src/runtime/routes/channel-verification-routes.ts +3 -3
- package/src/runtime/routes/contact-routes.ts +32 -8
- package/src/runtime/routes/conversation-cli-routes.ts +5 -4
- package/src/runtime/routes/conversation-list-routes.ts +7 -4
- package/src/runtime/routes/conversation-query-routes.ts +0 -72
- package/src/runtime/routes/conversation-routes.ts +85 -84
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +1 -3
- package/src/runtime/routes/guardian-action-routes.ts +5 -4
- package/src/runtime/routes/host-app-control-routes.ts +4 -5
- package/src/runtime/routes/host-bash-routes.ts +4 -5
- package/src/runtime/routes/host-browser-routes.ts +11 -9
- package/src/runtime/routes/host-cu-routes.ts +4 -5
- package/src/runtime/routes/host-file-routes.ts +4 -5
- package/src/runtime/routes/host-transfer-routes.ts +6 -6
- package/src/runtime/routes/http-adapter.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +2 -3
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +5 -97
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +49 -61
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -16
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +8 -21
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +3 -14
- package/src/runtime/routes/index.ts +0 -2
- package/src/runtime/routes/llm-context-normalization.ts +0 -83
- package/src/runtime/routes/mcp-auth-routes.ts +19 -171
- package/src/runtime/routes/migration-rollback-routes.ts +3 -4
- package/src/runtime/routes/migration-routes.ts +1 -4
- package/src/runtime/routes/subagents-routes.ts +0 -5
- package/src/runtime/routes/surface-action-routes.ts +56 -42
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +0 -1
- package/src/runtime/services/conversation-serializer.ts +9 -7
- package/src/runtime/tool-grant-request-helper.ts +3 -3
- package/src/runtime/trust-verdict-consumer.ts +9 -85
- package/src/runtime/verification-outbound-actions.ts +18 -18
- package/src/signals/user-message.ts +0 -16
- package/src/subagent/manager.ts +0 -9
- package/src/subagent/types.ts +3 -3
- package/src/telemetry/types.ts +1 -34
- package/src/telemetry/usage-telemetry-reporter.test.ts +2 -3
- package/src/telemetry/usage-telemetry-reporter.ts +3 -87
- package/src/tools/ask-question/ask-question-tool.test.ts +0 -29
- package/src/tools/ask-question/ask-question-tool.ts +0 -13
- package/src/tools/executor.ts +4 -4
- package/src/tools/registry.ts +0 -18
- package/src/tools/shared/filesystem/path-policy.ts +5 -12
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-defaults.ts +2 -9
- package/src/tools/tool-manifest.ts +0 -3
- package/src/tools/types.ts +2 -17
- package/src/tools/workspace-tools/loader.ts +244 -348
- package/src/util/errors.ts +1 -26
- package/src/util/platform.ts +0 -5
- package/src/workflows/library.test.ts +0 -140
- package/src/workflows/library.ts +28 -82
- package/src/workspace/migrations/017-seed-persona-dirs.ts +34 -3
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +24 -3
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +66 -14
- package/src/workspace/migrations/registry.ts +0 -2
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +0 -91
- package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +0 -48
- package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +0 -28
- package/node_modules/@vellumai/service-contracts/src/channels.ts +0 -41
- package/src/__tests__/code-search-tool.test.ts +0 -585
- package/src/__tests__/guardian-expiry-notifier.test.ts +0 -282
- package/src/__tests__/mcp-config-secret-boundary.test.ts +0 -390
- package/src/__tests__/plugin-pipeline.test.ts +0 -96
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +0 -102
- package/src/__tests__/steer-on-enqueue-question.test.ts +0 -181
- package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +0 -208
- package/src/agent/loop-exclusive-tool.test.ts +0 -150
- package/src/api/constants/sse-replay.ts +0 -41
- package/src/api/events/conversation-notice.ts +0 -26
- package/src/approvals/guardian-channel-delivery.ts +0 -30
- package/src/approvals/guardian-expiry-notifier.ts +0 -148
- package/src/cli/commands/memory/__tests__/worker.test.ts +0 -302
- package/src/cli/commands/memory/worker.ts +0 -175
- package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +0 -143
- package/src/config/prune-seeded-callsite-defaults.ts +0 -110
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -129
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +0 -312
- package/src/contacts/__tests__/member-write-relay.test.ts +0 -202
- package/src/contacts/guardian-delivery-reader.ts +0 -223
- package/src/contacts/member-write-relay.ts +0 -189
- package/src/daemon/conversation-notices.ts +0 -60
- package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -225
- package/src/hooks/hook-loader.ts +0 -341
- package/src/mcp/mcp-header-store.ts +0 -134
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +0 -110
- package/src/memory/__tests__/prompt-override.test.ts +0 -192
- package/src/memory/__tests__/watchdog-events-store.test.ts +0 -161
- package/src/memory/migrations/300-add-processing-started-at.ts +0 -30
- package/src/memory/migrations/301-create-watchdog-events.ts +0 -45
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +0 -224
- package/src/memory/prompt-override.ts +0 -129
- package/src/memory/steps.ts +0 -573
- package/src/memory/watchdog-events-store.ts +0 -87
- package/src/memory/worker-control.ts +0 -118
- package/src/memory/worker-process.ts +0 -72
- package/src/notifications/__tests__/connected-channels.test.ts +0 -114
- package/src/notifications/__tests__/destination-resolver.test.ts +0 -256
- package/src/onboarding/checkin-event.test.ts +0 -222
- package/src/onboarding/checkin-event.ts +0 -321
- package/src/onboarding/schedule-checkin.ts +0 -190
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
- package/src/plugins/defaults/advisor/context-pack.ts +0 -288
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +0 -146
- package/src/plugins/surface-import.ts +0 -121
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +0 -321
- package/src/providers/openai/api-error-normalization.ts +0 -270
- package/src/runtime/__tests__/channel-verification-service.test.ts +0 -133
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +0 -181
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +0 -66
- package/src/runtime/__tests__/local-principal-trust.test.ts +0 -164
- package/src/runtime/anchored-guardian.test.ts +0 -156
- package/src/runtime/anchored-guardian.ts +0 -135
- package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +0 -99
- package/src/runtime/local-principal-trust.ts +0 -52
- package/src/runtime/routes/__tests__/contact-routes.test.ts +0 -212
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -93
- package/src/runtime/routes/onboarding-checkin-routes.ts +0 -86
- package/src/tools/filesystem/search.ts +0 -543
- package/src/util/telemetry-db-path.ts +0 -24
- package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +0 -134
|
@@ -30,30 +30,13 @@
|
|
|
30
30
|
* → loadWorkspaceTools() ← this module (first scan)
|
|
31
31
|
* → loadUserPlugins()
|
|
32
32
|
* → bootstrapPlugins()
|
|
33
|
+
* → start file watcher ← hot register/unregister (no restart)
|
|
33
34
|
*
|
|
34
35
|
* Plugins load *after* the initial workspace-tool scan so the registry
|
|
35
36
|
* hands them a stable view of which workspace tools exist before any
|
|
36
|
-
* plugin code runs.
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* {@link loadWorkspaceTools} is idempotent and safe to call repeatedly:
|
|
41
|
-
* after the initial scan it reconciles the registry against on-disk
|
|
42
|
-
* state. Each call re-derives "given what's on disk right now under
|
|
43
|
-
* `tools/`, what registry state should the assistant be in?" and applies
|
|
44
|
-
* the delta — registering newly added tools, re-importing changed tools
|
|
45
|
-
* (mtime-gated, cache-busting via the per-import URL query string),
|
|
46
|
-
* unregistering deleted tools, stripping core tools when a `.removed`
|
|
47
|
-
* sentinel appears, and restoring them when it disappears.
|
|
48
|
-
*
|
|
49
|
-
* Instead of a long-lived filesystem watcher, the per-turn tool resolver
|
|
50
|
-
* (`createResolveToolsCallback` in `conversation-tool-setup.ts`) kicks this
|
|
51
|
-
* reconcile and then re-reads workspace tools from the registry — the same
|
|
52
|
-
* way it re-reads MCP tools — so a conversation picks up on-disk edits
|
|
53
|
-
* without a restart and without recreating the conversation. The "edit a
|
|
54
|
-
* file, see the change" loop closes on the next turn. Unchanged files are
|
|
55
|
-
* skipped via the mtime cache, so a no-op reconcile costs one `readdir`
|
|
56
|
-
* plus a `stat` per file and never re-imports.
|
|
37
|
+
* plugin code runs. The file watcher then runs for the lifetime of the
|
|
38
|
+
* assistant, picking up add/change/delete events to keep the registry
|
|
39
|
+
* in sync with disk.
|
|
57
40
|
*
|
|
58
41
|
* Per-tool isolation:
|
|
59
42
|
*
|
|
@@ -79,19 +62,12 @@
|
|
|
79
62
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
80
63
|
import { readFile } from "node:fs/promises";
|
|
81
64
|
import { extname, join } from "node:path";
|
|
65
|
+
import { pathToFileURL } from "node:url";
|
|
82
66
|
|
|
83
67
|
import { getLogger } from "../../util/logger.js";
|
|
84
68
|
import { getWorkspaceToolsDir } from "../../util/platform.js";
|
|
85
69
|
import { isProviderSafeToolName } from "../provider-tool-name.js";
|
|
86
|
-
import {
|
|
87
|
-
getCoreToolOverride,
|
|
88
|
-
getTool,
|
|
89
|
-
getToolOwner,
|
|
90
|
-
registerWorkspaceTools,
|
|
91
|
-
removeCoreToolViaWorkspace,
|
|
92
|
-
restoreStrippedCoreTool,
|
|
93
|
-
unregisterWorkspaceTool,
|
|
94
|
-
} from "../registry.js";
|
|
70
|
+
import { registerWorkspaceTools, removeCoreToolViaWorkspace } from "../registry.js";
|
|
95
71
|
import { finalizeTool } from "../tool-defaults.js";
|
|
96
72
|
import type {
|
|
97
73
|
RiskLevel,
|
|
@@ -163,21 +139,14 @@ function isValidToolFilenameStem(stem: string): boolean {
|
|
|
163
139
|
*/
|
|
164
140
|
function classifyEntry(
|
|
165
141
|
entry: string,
|
|
166
|
-
):
|
|
167
|
-
| { kind: "live"; stem: string; ext: LiveToolExtension }
|
|
168
|
-
| { kind: "removed"; stem: string }
|
|
169
|
-
| undefined {
|
|
142
|
+
): { kind: "live"; stem: string; ext: LiveToolExtension } | { kind: "removed"; stem: string } | undefined {
|
|
170
143
|
const ext = extname(entry);
|
|
171
144
|
if (ext === REMOVED_EXTENSION) {
|
|
172
145
|
return { kind: "removed", stem: entry.slice(0, -REMOVED_EXTENSION.length) };
|
|
173
146
|
}
|
|
174
147
|
for (const candidate of LIVE_TOOL_EXTENSIONS) {
|
|
175
148
|
if (ext === candidate) {
|
|
176
|
-
return {
|
|
177
|
-
kind: "live",
|
|
178
|
-
stem: entry.slice(0, -candidate.length),
|
|
179
|
-
ext: candidate,
|
|
180
|
-
};
|
|
149
|
+
return { kind: "live", stem: entry.slice(0, -candidate.length), ext: candidate };
|
|
181
150
|
}
|
|
182
151
|
}
|
|
183
152
|
return undefined;
|
|
@@ -193,9 +162,7 @@ interface LiveSelection {
|
|
|
193
162
|
shadowed: LiveToolExtension[];
|
|
194
163
|
}
|
|
195
164
|
|
|
196
|
-
function selectLiveExtension(
|
|
197
|
-
extensions: Set<LiveToolExtension>,
|
|
198
|
-
): LiveSelection {
|
|
165
|
+
function selectLiveExtension(extensions: Set<LiveToolExtension>): LiveSelection {
|
|
199
166
|
for (const candidate of LIVE_TOOL_EXTENSIONS) {
|
|
200
167
|
if (extensions.has(candidate)) {
|
|
201
168
|
const shadowed: LiveToolExtension[] = [];
|
|
@@ -225,19 +192,14 @@ function selectLiveExtension(
|
|
|
225
192
|
* The tool still loads cleanly with these defaults — a broken tool must
|
|
226
193
|
* never block daemon boot. Always sets `category: "workspace"` so the
|
|
227
194
|
* registry can distinguish workspace overrides from other origins.
|
|
228
|
-
*
|
|
229
|
-
* The registered name is pinned to the filename stem (`name`), overriding
|
|
230
|
-
* any `name` field on the file's own export. This is the documented
|
|
231
|
-
* "filename stem is the tool name verbatim" contract — `finalizeTool`
|
|
232
|
-
* would otherwise prefer `tool.name` — and it keeps the registered name in
|
|
233
|
-
* lockstep with the stem the reconcile keys its mtime cache by, so a later
|
|
234
|
-
* delete of the file unregisters the right tool.
|
|
235
195
|
*/
|
|
236
|
-
function applyWorkspaceToolDefaults(
|
|
196
|
+
function applyWorkspaceToolDefaults(
|
|
197
|
+
tool: ToolDefinition,
|
|
198
|
+
name: string,
|
|
199
|
+
): Tool {
|
|
237
200
|
const finalized = finalizeTool(
|
|
238
201
|
{
|
|
239
202
|
...tool,
|
|
240
|
-
name,
|
|
241
203
|
defaultRiskLevel:
|
|
242
204
|
tool.defaultRiskLevel ?? WORKSPACE_TOOL_DEFAULTS.defaultRiskLevel,
|
|
243
205
|
category: tool.category ?? "workspace",
|
|
@@ -259,18 +221,10 @@ function applyWorkspaceToolDefaults(tool: ToolDefinition, name: string): Tool {
|
|
|
259
221
|
* module's default export, or `undefined` if the import times out, has
|
|
260
222
|
* no default export, or throws.
|
|
261
223
|
*
|
|
262
|
-
* A cache-busting `?v=<counter>` query string is appended so
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* gets a fresh module identity.
|
|
266
|
-
*
|
|
267
|
-
* The specifier is the raw absolute path (not a `file://` URL): bun honors
|
|
268
|
-
* the `?v=` query for cache-busting on a bare absolute path but collapses
|
|
269
|
-
* it to the same cached module for a `file://` URL, which would silently
|
|
270
|
-
* serve stale source on re-import. Absolute paths (and embedded spaces)
|
|
271
|
-
* import cleanly; only `?`/`#` in the path would confuse the query, and
|
|
272
|
-
* tool stems are provider-safe so the directory prefix is the only place
|
|
273
|
-
* those could appear.
|
|
224
|
+
* A cache-busting `?v=<counter>` query string is appended so the loader's
|
|
225
|
+
* later re-imports (driven by the file watcher) pick up disk changes
|
|
226
|
+
* instead of node's cached module. The counter is per-call, so every
|
|
227
|
+
* import gets a fresh module identity.
|
|
274
228
|
*
|
|
275
229
|
* All failure paths log with file attribution so operators can find the
|
|
276
230
|
* broken tool quickly.
|
|
@@ -281,7 +235,7 @@ async function importToolDefaultBounded(
|
|
|
281
235
|
entryPath: string,
|
|
282
236
|
timeoutMs: number,
|
|
283
237
|
): Promise<unknown> {
|
|
284
|
-
const url = `${entryPath}?v=${++importCounter}`;
|
|
238
|
+
const url = `${pathToFileURL(entryPath).href}?v=${++importCounter}`;
|
|
285
239
|
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
286
240
|
try {
|
|
287
241
|
const timeoutSentinel = Symbol("workspace-tool-import-timeout");
|
|
@@ -331,9 +285,7 @@ async function importToolDefaultBounded(
|
|
|
331
285
|
* exist for declarative use cases — schema-only tool stubs, override
|
|
332
286
|
* placeholders, etc.
|
|
333
287
|
*/
|
|
334
|
-
async function readJsonToolSpec(
|
|
335
|
-
entryPath: string,
|
|
336
|
-
): Promise<ToolDefinition | undefined> {
|
|
288
|
+
async function readJsonToolSpec(entryPath: string): Promise<ToolDefinition | undefined> {
|
|
337
289
|
let raw: string;
|
|
338
290
|
try {
|
|
339
291
|
raw = await readFile(entryPath, "utf8");
|
|
@@ -374,62 +326,53 @@ export interface LoadWorkspaceToolsOptions {
|
|
|
374
326
|
}
|
|
375
327
|
|
|
376
328
|
/**
|
|
377
|
-
* Result of a {@link loadWorkspaceTools} call —
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* reconcile applied its delta.
|
|
329
|
+
* Result of a {@link loadWorkspaceTools} call — exposes which tool names
|
|
330
|
+
* were registered and which were stripped so callers (notably the file
|
|
331
|
+
* watcher) can compute deltas against subsequent re-scans.
|
|
381
332
|
*/
|
|
382
333
|
export interface LoadWorkspaceToolsResult {
|
|
383
|
-
/** Tool names
|
|
334
|
+
/** Tool names successfully registered as workspace tools. */
|
|
384
335
|
readonly registered: string[];
|
|
385
|
-
/**
|
|
336
|
+
/** Tool names stripped from the registry via `<name>.removed` sentinels. */
|
|
386
337
|
readonly removed: string[];
|
|
387
338
|
}
|
|
388
339
|
|
|
389
340
|
/**
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
404
|
-
*
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
*
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Pure (no registry mutation) scan of `<workspaceDir>/tools/`. Resolves
|
|
419
|
-
* each stem to its winning live file (with mtime) and the set of stems
|
|
420
|
-
* carrying a `.removed` sentinel, applying the same validation and
|
|
421
|
-
* shadow/ambiguity rules the reconcile relies on. Returns empty maps when
|
|
422
|
-
* the directory is missing or unreadable.
|
|
341
|
+
* Scan `<workspaceDir>/tools/` and register every well-formed
|
|
342
|
+
* `<name>.{ts,js,json}` as a workspace tool. Files matching
|
|
343
|
+
* `<name>.removed` strip the core tool of that name from the registry
|
|
344
|
+
* via {@link removeCoreToolViaWorkspace}.
|
|
345
|
+
*
|
|
346
|
+
* Invariants:
|
|
347
|
+
*
|
|
348
|
+
* - No-ops silently when the tools directory does not exist. A clean
|
|
349
|
+
* install with zero workspace tools must produce no log noise beyond
|
|
350
|
+
* the eventual "0 workspace tools registered" debug line.
|
|
351
|
+
* - Per-tool isolation: any single broken tool is logged and skipped
|
|
352
|
+
* without aborting the scan. The function resolves normally even when
|
|
353
|
+
* every candidate fails.
|
|
354
|
+
* - Idempotency is the registry's job: a second call without a
|
|
355
|
+
* preceding unregister will throw on the duplicate-workspace-tool
|
|
356
|
+
* check. Callers (daemon startup) are expected to call once; the file
|
|
357
|
+
* watcher uses the per-event entry points instead.
|
|
358
|
+
*
|
|
359
|
+
* Caller responsibilities:
|
|
360
|
+
*
|
|
361
|
+
* - Must be invoked between {@link initializeTools} and
|
|
362
|
+
* {@link loadUserPlugins}. Calling earlier risks racing core
|
|
363
|
+
* registrations; calling later means plugins see an incomplete
|
|
364
|
+
* registry and may register over a name a workspace tool will later
|
|
365
|
+
* try to own.
|
|
423
366
|
*/
|
|
424
|
-
function
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
const removedStems = new Set<string>();
|
|
367
|
+
export async function loadWorkspaceTools(
|
|
368
|
+
options: LoadWorkspaceToolsOptions = {},
|
|
369
|
+
): Promise<LoadWorkspaceToolsResult> {
|
|
370
|
+
const importTimeoutMs = options.importTimeoutMs ?? IMPORT_TIMEOUT_MS;
|
|
371
|
+
const toolsDir = getWorkspaceToolsDir();
|
|
430
372
|
|
|
431
373
|
if (!existsSync(toolsDir)) {
|
|
432
|
-
|
|
374
|
+
log.debug({ toolsDir }, "Workspace tools directory does not exist — skipping");
|
|
375
|
+
return { registered: [], removed: [] };
|
|
433
376
|
}
|
|
434
377
|
|
|
435
378
|
let entries: string[];
|
|
@@ -440,15 +383,16 @@ function scanWorkspaceToolsDir(toolsDir: string): {
|
|
|
440
383
|
{ err, toolsDir },
|
|
441
384
|
"loadWorkspaceTools: failed to read tools directory — continuing with no workspace tools",
|
|
442
385
|
);
|
|
443
|
-
return {
|
|
386
|
+
return { registered: [], removed: [] };
|
|
444
387
|
}
|
|
445
388
|
|
|
446
389
|
// Group entries by stem so we can detect multi-extension shadowing
|
|
447
390
|
// (e.g. `foo.ts` + `foo.js` claiming the same name) before we kick off
|
|
448
|
-
// any imports. Each stem maps to
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
const liveByStem = new Map<string,
|
|
391
|
+
// any imports. Each stem maps to a Set of extensions; .removed sentinels
|
|
392
|
+
// are tracked in a separate set since they're mutually exclusive with
|
|
393
|
+
// live tool files (you don't strip AND register a name at once).
|
|
394
|
+
const liveByStem = new Map<string, Set<LiveToolExtension>>();
|
|
395
|
+
const removedStems = new Set<string>();
|
|
452
396
|
|
|
453
397
|
for (const entry of entries) {
|
|
454
398
|
const fullPath = join(toolsDir, entry);
|
|
@@ -485,10 +429,10 @@ function scanWorkspaceToolsDir(toolsDir: string): {
|
|
|
485
429
|
}
|
|
486
430
|
let extensions = liveByStem.get(classified.stem);
|
|
487
431
|
if (!extensions) {
|
|
488
|
-
extensions = new
|
|
432
|
+
extensions = new Set<LiveToolExtension>();
|
|
489
433
|
liveByStem.set(classified.stem, extensions);
|
|
490
434
|
}
|
|
491
|
-
extensions.
|
|
435
|
+
extensions.add(classified.ext);
|
|
492
436
|
}
|
|
493
437
|
|
|
494
438
|
// A stem cannot both be live AND removed. Operator intent is ambiguous;
|
|
@@ -504,274 +448,226 @@ function scanWorkspaceToolsDir(toolsDir: string): {
|
|
|
504
448
|
}
|
|
505
449
|
}
|
|
506
450
|
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
|
|
451
|
+
// Apply removals before registrations so the batch validation in
|
|
452
|
+
// registerWorkspaceTools sees the post-removal registry state.
|
|
453
|
+
const removed: string[] = [];
|
|
454
|
+
for (const stem of removedStems) {
|
|
455
|
+
try {
|
|
456
|
+
removeCoreToolViaWorkspace(stem);
|
|
457
|
+
removed.push(stem);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
460
|
+
log.error(
|
|
461
|
+
{ err, stem },
|
|
462
|
+
`loadWorkspaceTools: failed to strip core tool "${stem}": ${message}`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Resolve each live stem to its winning entry, import it, and add to
|
|
468
|
+
// the registration batch. Multi-extension shadowing warns once per
|
|
469
|
+
// ignored sibling so the operator can clean up the redundant file.
|
|
470
|
+
const batch: Array<{ tool: Tool; workspacePath: string }> = [];
|
|
471
|
+
|
|
510
472
|
for (const [stem, extensions] of liveByStem) {
|
|
511
|
-
const { ext: winningExt, shadowed } = selectLiveExtension(
|
|
512
|
-
new Set(extensions.keys()),
|
|
513
|
-
);
|
|
473
|
+
const { ext: winningExt, shadowed } = selectLiveExtension(extensions);
|
|
514
474
|
if (shadowed.length > 0) {
|
|
515
475
|
log.warn(
|
|
516
476
|
{ stem, winningExt, shadowed, toolsDir },
|
|
517
477
|
`loadWorkspaceTools: "${stem}" has multiple files (${[winningExt, ...shadowed].join(", ")}) — using ${winningExt} and ignoring the rest`,
|
|
518
478
|
);
|
|
519
479
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
480
|
+
const entryPath = join(toolsDir, `${stem}${winningExt}`);
|
|
481
|
+
|
|
482
|
+
let toolSpec: ToolDefinition | undefined;
|
|
483
|
+
if (winningExt === ".json") {
|
|
484
|
+
toolSpec = await readJsonToolSpec(entryPath);
|
|
485
|
+
} else {
|
|
486
|
+
const defaultExport = await importToolDefaultBounded(entryPath, importTimeoutMs);
|
|
487
|
+
if (defaultExport === undefined) continue; // Failure already logged.
|
|
488
|
+
if (defaultExport === null || typeof defaultExport !== "object") {
|
|
489
|
+
log.error(
|
|
490
|
+
{ entryPath, type: typeof defaultExport },
|
|
491
|
+
`Workspace tool at ${entryPath} default export must be an object — skipping`,
|
|
492
|
+
);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
toolSpec = defaultExport as ToolDefinition;
|
|
496
|
+
}
|
|
497
|
+
if (!toolSpec) continue;
|
|
498
|
+
|
|
499
|
+
const loaded = applyWorkspaceToolDefaults(toolSpec, stem);
|
|
500
|
+
batch.push({ tool: loaded, workspacePath: entryPath });
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (batch.length === 0) {
|
|
504
|
+
if (removed.length === 0) {
|
|
505
|
+
log.debug(
|
|
506
|
+
{ toolsDir },
|
|
507
|
+
"loadWorkspaceTools: no workspace tools to register or strip",
|
|
508
|
+
);
|
|
509
|
+
} else {
|
|
510
|
+
log.info(
|
|
511
|
+
{ toolsDir, removedCount: removed.length, removed },
|
|
512
|
+
`Stripped ${removed.length} core tool${removed.length === 1 ? "" : "s"} via workspace .removed sentinels`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return { registered: [], removed };
|
|
525
516
|
}
|
|
526
517
|
|
|
527
|
-
|
|
518
|
+
try {
|
|
519
|
+
const accepted = registerWorkspaceTools(batch);
|
|
520
|
+
log.info(
|
|
521
|
+
{ count: accepted.length, toolsDir, removedCount: removed.length },
|
|
522
|
+
`Registered ${accepted.length} workspace tool${accepted.length === 1 ? "" : "s"}${removed.length > 0 ? ` (and stripped ${removed.length} core tool${removed.length === 1 ? "" : "s"})` : ""}`,
|
|
523
|
+
);
|
|
524
|
+
return { registered: accepted.map((t) => t.name), removed };
|
|
525
|
+
} catch (err) {
|
|
526
|
+
// A throw from registerWorkspaceTools means a hard conflict (e.g.
|
|
527
|
+
// duplicate name in batch, lifecycle-order regression). The batch
|
|
528
|
+
// validation phase guarantees no partial application landed, so
|
|
529
|
+
// every workspace tool from this load attempt is absent from the
|
|
530
|
+
// registry. Surface the error loudly but do NOT rethrow — assistant
|
|
531
|
+
// startup must still complete.
|
|
532
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
533
|
+
log.error(
|
|
534
|
+
{ err, toolsDir, batchSize: batch.length },
|
|
535
|
+
`loadWorkspaceTools: registry rejected batch — ${message}`,
|
|
536
|
+
);
|
|
537
|
+
return { registered: [], removed };
|
|
538
|
+
}
|
|
528
539
|
}
|
|
529
540
|
|
|
541
|
+
// ─── Single-entry helpers for the file watcher ───────────────────────────────
|
|
542
|
+
//
|
|
543
|
+
// The watcher calls these on each fs event. The initial-scan path
|
|
544
|
+
// ({@link loadWorkspaceTools}) batches for transactional registration;
|
|
545
|
+
// the per-event path takes the simpler one-tool-at-a-time route since
|
|
546
|
+
// fs events arrive serially and the registry handles each as an
|
|
547
|
+
// atomic operation.
|
|
548
|
+
|
|
530
549
|
/**
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
550
|
+
* Load and register a single workspace tool file. Returns the registered
|
|
551
|
+
* tool name on success or `undefined` if the file failed to load (errors
|
|
552
|
+
* are logged with file attribution and never thrown to the caller).
|
|
553
|
+
*
|
|
554
|
+
* Used by the file watcher's `add` event. The caller is expected to
|
|
555
|
+
* have already unregistered any prior workspace tool for the same name.
|
|
536
556
|
*/
|
|
537
|
-
function
|
|
538
|
-
|
|
539
|
-
|
|
557
|
+
export async function loadSingleWorkspaceTool(
|
|
558
|
+
entryPath: string,
|
|
559
|
+
options: LoadWorkspaceToolsOptions = {},
|
|
560
|
+
): Promise<string | undefined> {
|
|
561
|
+
const importTimeoutMs = options.importTimeoutMs ?? IMPORT_TIMEOUT_MS;
|
|
562
|
+
const filename = entryPath.split("/").pop() ?? "";
|
|
563
|
+
const classified = classifyEntry(filename);
|
|
564
|
+
if (!classified || classified.kind !== "live") {
|
|
565
|
+
log.debug(
|
|
566
|
+
{ entryPath },
|
|
567
|
+
"loadSingleWorkspaceTool: file is not a live tool entry — skipping",
|
|
568
|
+
);
|
|
569
|
+
return undefined;
|
|
540
570
|
}
|
|
541
|
-
if (
|
|
542
|
-
|
|
571
|
+
if (!isValidToolFilenameStem(classified.stem)) {
|
|
572
|
+
log.error(
|
|
573
|
+
{ entryPath, stem: classified.stem },
|
|
574
|
+
`loadSingleWorkspaceTool: filename stem "${classified.stem}" is not a provider-safe tool name — skipping`,
|
|
575
|
+
);
|
|
576
|
+
return undefined;
|
|
543
577
|
}
|
|
544
|
-
}
|
|
545
578
|
|
|
546
|
-
/**
|
|
547
|
-
* Import and finalize the winning live file for `stem`, returning the
|
|
548
|
-
* registry-ready {@link Tool} or `undefined` when the file fails to load
|
|
549
|
-
* (every failure is logged with file attribution and never thrown).
|
|
550
|
-
*/
|
|
551
|
-
async function loadDesiredLiveTool(
|
|
552
|
-
stem: string,
|
|
553
|
-
entry: DesiredLiveEntry,
|
|
554
|
-
importTimeoutMs: number,
|
|
555
|
-
): Promise<Tool | undefined> {
|
|
556
579
|
let toolSpec: ToolDefinition | undefined;
|
|
557
|
-
if (
|
|
558
|
-
toolSpec = await readJsonToolSpec(
|
|
580
|
+
if (classified.ext === ".json") {
|
|
581
|
+
toolSpec = await readJsonToolSpec(entryPath);
|
|
559
582
|
} else {
|
|
560
|
-
const defaultExport = await importToolDefaultBounded(
|
|
561
|
-
|
|
562
|
-
importTimeoutMs,
|
|
563
|
-
);
|
|
564
|
-
if (defaultExport === undefined) return undefined; // Failure already logged.
|
|
583
|
+
const defaultExport = await importToolDefaultBounded(entryPath, importTimeoutMs);
|
|
584
|
+
if (defaultExport === undefined) return undefined;
|
|
565
585
|
if (defaultExport === null || typeof defaultExport !== "object") {
|
|
566
586
|
log.error(
|
|
567
|
-
{ entryPath
|
|
568
|
-
`Workspace tool at ${
|
|
587
|
+
{ entryPath, type: typeof defaultExport },
|
|
588
|
+
`Workspace tool at ${entryPath} default export must be an object — skipping`,
|
|
569
589
|
);
|
|
570
590
|
return undefined;
|
|
571
591
|
}
|
|
572
592
|
toolSpec = defaultExport as ToolDefinition;
|
|
573
593
|
}
|
|
574
594
|
if (!toolSpec) return undefined;
|
|
575
|
-
|
|
595
|
+
|
|
596
|
+
const loaded = applyWorkspaceToolDefaults(toolSpec, classified.stem);
|
|
597
|
+
try {
|
|
598
|
+
registerWorkspaceTools([{ tool: loaded, workspacePath: entryPath }]);
|
|
599
|
+
return classified.stem;
|
|
600
|
+
} catch (err) {
|
|
601
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
602
|
+
log.error(
|
|
603
|
+
{ err, entryPath },
|
|
604
|
+
`loadSingleWorkspaceTool: registry rejected "${classified.stem}": ${message}`,
|
|
605
|
+
);
|
|
606
|
+
return undefined;
|
|
607
|
+
}
|
|
576
608
|
}
|
|
577
609
|
|
|
578
610
|
/**
|
|
579
|
-
*
|
|
580
|
-
* it
|
|
581
|
-
* pile up or interleave their unregister/register sequences against the
|
|
582
|
-
* shared registry. Once it settles this is cleared, so a later caller (the
|
|
583
|
-
* next turn, or a sequential awaiter like boot/tests) starts a fresh scan.
|
|
611
|
+
* Classify a single filesystem entry. Exposed for the file watcher so
|
|
612
|
+
* it can route events without re-implementing the extension logic.
|
|
584
613
|
*/
|
|
585
|
-
|
|
614
|
+
export function classifyWorkspaceToolEntry(
|
|
615
|
+
filename: string,
|
|
616
|
+
):
|
|
617
|
+
| { kind: "live"; stem: string; ext: LiveToolExtension }
|
|
618
|
+
| { kind: "removed"; stem: string }
|
|
619
|
+
| undefined {
|
|
620
|
+
return classifyEntry(filename);
|
|
621
|
+
}
|
|
586
622
|
|
|
587
623
|
/**
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
* Idempotent and safe to call repeatedly: the first call registers every
|
|
592
|
-
* well-formed `<name>.{ts,js,json}` as a workspace tool and strips core
|
|
593
|
-
* tools named by `<name>.removed` sentinels; subsequent calls apply only
|
|
594
|
-
* the delta since the previous reconcile — registering added files,
|
|
595
|
-
* re-importing changed files (detected by mtime), unregistering deleted
|
|
596
|
-
* files, and restoring core tools whose sentinel was removed.
|
|
597
|
-
*
|
|
598
|
-
* Invariants:
|
|
599
|
-
*
|
|
600
|
-
* - No-ops to an empty registry footprint when the tools directory does
|
|
601
|
-
* not exist, tearing down anything a previous reconcile installed.
|
|
602
|
-
* - Per-tool isolation: any single broken tool is logged and skipped
|
|
603
|
-
* without aborting the reconcile. The function resolves normally even
|
|
604
|
-
* when every candidate fails.
|
|
605
|
-
* - Concurrency-safe: concurrent callers coalesce onto a single in-flight
|
|
606
|
-
* reconcile, so the unregister/register sequence for a changed tool never
|
|
607
|
-
* races another reconcile.
|
|
624
|
+
* Scan `toolsDir` for all entries matching `stem` and return the winning
|
|
625
|
+
* live file's absolute path (if any) plus whether a `.removed` sentinel
|
|
626
|
+
* exists for the same stem.
|
|
608
627
|
*
|
|
609
|
-
*
|
|
610
|
-
*
|
|
611
|
-
*
|
|
612
|
-
*
|
|
613
|
-
* registrations; calling later means plugins see an incomplete
|
|
614
|
-
* registry and may register over a name a workspace tool will later
|
|
615
|
-
* try to own. Later calls (driven by the per-turn tool resolver) are
|
|
616
|
-
* free to run any time — the reconcile only ever touches
|
|
617
|
-
* workspace-owned and core-stashed names.
|
|
628
|
+
* Multi-extension precedence: `.js` > `.ts` > `.json`. Shadowed siblings
|
|
629
|
+
* are not reported here — the caller decides whether to warn (the full
|
|
630
|
+
* scan path does; the per-stem watcher does not, because shadow events
|
|
631
|
+
* are noisy in editor save flows).
|
|
618
632
|
*/
|
|
619
|
-
export function
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
// fresh.
|
|
626
|
-
inflightReconcile = reconcileWorkspaceTools(options).finally(() => {
|
|
627
|
-
inflightReconcile = null;
|
|
628
|
-
});
|
|
629
|
-
return inflightReconcile;
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
async function reconcileWorkspaceTools(
|
|
633
|
-
options: LoadWorkspaceToolsOptions,
|
|
634
|
-
): Promise<LoadWorkspaceToolsResult> {
|
|
635
|
-
const importTimeoutMs = options.importTimeoutMs ?? IMPORT_TIMEOUT_MS;
|
|
636
|
-
const toolsDir = getWorkspaceToolsDir();
|
|
637
|
-
|
|
638
|
-
const { desiredLive, removedStems } = scanWorkspaceToolsDir(toolsDir);
|
|
639
|
-
|
|
640
|
-
// Snapshot what we managed before so we can detect stems that vanished
|
|
641
|
-
// from disk entirely (present last time, absent now) and tear them down.
|
|
642
|
-
const prevManaged = new Map(managed);
|
|
643
|
-
|
|
644
|
-
// 1. Tear down stems we previously managed that disk no longer mentions
|
|
645
|
-
// (neither a live file nor a .removed sentinel). Stems still present
|
|
646
|
-
// are handled by the live/removed passes below.
|
|
647
|
-
for (const stem of prevManaged.keys()) {
|
|
648
|
-
if (!desiredLive.has(stem) && !removedStems.has(stem)) {
|
|
649
|
-
teardownStem(stem);
|
|
650
|
-
managed.delete(stem);
|
|
651
|
-
}
|
|
633
|
+
export function findWinningWorkspaceToolPath(
|
|
634
|
+
toolsDir: string,
|
|
635
|
+
stem: string,
|
|
636
|
+
): { livePath: string | null; liveExt: LiveToolExtension | null; hasRemovedSentinel: boolean } {
|
|
637
|
+
if (!existsSync(toolsDir)) {
|
|
638
|
+
return { livePath: null, liveExt: null, hasRemovedSentinel: false };
|
|
652
639
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
removeCoreToolViaWorkspace(stem);
|
|
663
|
-
managed.set(stem, { kind: "removed" });
|
|
664
|
-
} catch (err) {
|
|
665
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
666
|
-
log.error(
|
|
667
|
-
{ err, stem },
|
|
668
|
-
`loadWorkspaceTools: failed to strip core tool "${stem}": ${message}`,
|
|
669
|
-
);
|
|
670
|
-
managed.delete(stem);
|
|
671
|
-
}
|
|
640
|
+
let entries: string[];
|
|
641
|
+
try {
|
|
642
|
+
entries = readdirSync(toolsDir);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
log.warn(
|
|
645
|
+
{ err, toolsDir, stem },
|
|
646
|
+
"findWinningWorkspaceToolPath: failed to read tools directory",
|
|
647
|
+
);
|
|
648
|
+
return { livePath: null, liveExt: null, hasRemovedSentinel: false };
|
|
672
649
|
}
|
|
673
650
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
// on its own so one broken or conflicting file cannot drop the others
|
|
677
|
-
// (per-tool isolation): the import is bounded/caught, and registration
|
|
678
|
-
// goes through registerWorkspaceTools one tool at a time. Stems are the
|
|
679
|
-
// map keys here, so there are no intra-reconcile duplicate names that a
|
|
680
|
-
// batch would otherwise need to validate.
|
|
681
|
-
for (const [stem, entry] of desiredLive) {
|
|
682
|
-
const prev = prevManaged.get(stem);
|
|
683
|
-
const unchanged =
|
|
684
|
-
prev?.kind === "live" &&
|
|
685
|
-
prev.ext === entry.ext &&
|
|
686
|
-
prev.mtimeMs === entry.mtimeMs &&
|
|
687
|
-
getToolOwner(stem)?.kind === "workspace";
|
|
688
|
-
if (unchanged) {
|
|
689
|
-
managed.set(stem, {
|
|
690
|
-
kind: "live",
|
|
691
|
-
ext: entry.ext,
|
|
692
|
-
mtimeMs: entry.mtimeMs,
|
|
693
|
-
});
|
|
694
|
-
continue;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// Changed, or a fresh import is needed. Import FIRST and only mutate the
|
|
698
|
-
// registry once we hold a valid tool, so a failed re-import leaves the
|
|
699
|
-
// previously-registered version in place rather than tearing it down.
|
|
700
|
-
const tool = await loadDesiredLiveTool(stem, entry, importTimeoutMs);
|
|
701
|
-
if (!tool) {
|
|
702
|
-
// Import failed (already logged). Leave any prior registration intact
|
|
703
|
-
// and keep the managed entry so a later fix re-imports cleanly.
|
|
704
|
-
continue;
|
|
705
|
-
}
|
|
651
|
+
const liveExtensions = new Set<LiveToolExtension>();
|
|
652
|
+
let hasRemovedSentinel = false;
|
|
706
653
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
if (
|
|
711
|
-
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
restoreStrippedCoreTool(stem);
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
try {
|
|
718
|
-
registerWorkspaceTools([{ tool, workspacePath: entry.path }]);
|
|
719
|
-
managed.set(stem, {
|
|
720
|
-
kind: "live",
|
|
721
|
-
ext: entry.ext,
|
|
722
|
-
mtimeMs: entry.mtimeMs,
|
|
723
|
-
});
|
|
724
|
-
} catch (err) {
|
|
725
|
-
// A throw means a hard conflict for this name (e.g. a plugin/MCP tool
|
|
726
|
-
// already owns it — a lifecycle-order regression). Surface it loudly,
|
|
727
|
-
// but do NOT rethrow and do NOT abort the other tools — startup /
|
|
728
|
-
// conversation reads must still complete.
|
|
729
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
730
|
-
log.error(
|
|
731
|
-
{ err, stem, toolsDir },
|
|
732
|
-
`loadWorkspaceTools: registry rejected "${stem}" — ${message}`,
|
|
733
|
-
);
|
|
734
|
-
managed.delete(stem);
|
|
654
|
+
for (const entry of entries) {
|
|
655
|
+
const classified = classifyEntry(entry);
|
|
656
|
+
if (!classified || classified.stem !== stem) continue;
|
|
657
|
+
if (classified.kind === "removed") {
|
|
658
|
+
hasRemovedSentinel = true;
|
|
659
|
+
} else {
|
|
660
|
+
liveExtensions.add(classified.ext);
|
|
735
661
|
}
|
|
736
662
|
}
|
|
737
663
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const registered: string[] = [];
|
|
741
|
-
const removed: string[] = [];
|
|
742
|
-
for (const [stem, entry] of managed) {
|
|
743
|
-
if (entry.kind === "live" && getToolOwner(stem)?.kind === "workspace") {
|
|
744
|
-
registered.push(stem);
|
|
745
|
-
} else if (
|
|
746
|
-
entry.kind === "removed" &&
|
|
747
|
-
getCoreToolOverride(stem) &&
|
|
748
|
-
!getTool(stem)
|
|
749
|
-
) {
|
|
750
|
-
removed.push(stem);
|
|
751
|
-
}
|
|
664
|
+
if (liveExtensions.size === 0) {
|
|
665
|
+
return { livePath: null, liveExt: null, hasRemovedSentinel };
|
|
752
666
|
}
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
} else {
|
|
760
|
-
log.info(
|
|
761
|
-
{ count: registered.length, toolsDir, removedCount: removed.length },
|
|
762
|
-
`Workspace tools reconciled: ${registered.length} registered${removed.length > 0 ? `, ${removed.length} core tool${removed.length === 1 ? "" : "s"} stripped` : ""}`,
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
return { registered, removed };
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
/**
|
|
770
|
-
* Test-only — drop the mtime cache and serialization chain so a fresh
|
|
771
|
-
* test starts from a clean reconcile baseline. The registry itself is
|
|
772
|
-
* reset separately via `__clearRegistryForTesting`.
|
|
773
|
-
*/
|
|
774
|
-
export function __resetWorkspaceToolCacheForTesting(): void {
|
|
775
|
-
managed.clear();
|
|
776
|
-
inflightReconcile = null;
|
|
667
|
+
const { ext } = selectLiveExtension(liveExtensions);
|
|
668
|
+
return {
|
|
669
|
+
livePath: join(toolsDir, `${stem}${ext}`),
|
|
670
|
+
liveExt: ext,
|
|
671
|
+
hasRemovedSentinel,
|
|
672
|
+
};
|
|
777
673
|
}
|