@vellumai/assistant 0.8.10 → 0.8.11-staging.1
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 +62 -1
- package/docs/workspace-tools.md +196 -0
- package/examples/plugins/echo/README.md +3 -3
- package/knip.json +1 -0
- package/openapi.yaml +460 -128
- package/package.json +2 -1
- package/scripts/build-plugin-api.ts +299 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
- package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
- package/src/__tests__/agent-loop-thinking.test.ts +4 -0
- package/src/__tests__/agent-loop.test.ts +578 -5
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
- package/src/__tests__/approval-cascade.test.ts +1 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
- package/src/__tests__/btw-routes.test.ts +0 -1
- package/src/__tests__/build-persisted-content.test.ts +75 -1
- package/src/__tests__/catalog-install-normalize.test.ts +141 -0
- package/src/__tests__/ces-startup-timeout.test.ts +60 -0
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +264 -124
- package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
- package/src/__tests__/conversation-agent-loop.test.ts +131 -551
- package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
- package/src/__tests__/conversation-fork-crud.test.ts +259 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-lifecycle.test.ts +257 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
- package/src/__tests__/conversation-queue.test.ts +1 -39
- package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
- package/src/__tests__/conversation-skill-tools.test.ts +491 -5
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +1 -0
- package/src/__tests__/conversation-store.test.ts +74 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
- package/src/__tests__/credential-execution-tools.test.ts +23 -11
- package/src/__tests__/credential-security-invariants.test.ts +6 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
- package/src/__tests__/device-id.test.ts +70 -1
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- package/src/__tests__/empty-response-hook.test.ts +242 -66
- package/src/__tests__/external-plugin-loader.test.ts +0 -31
- package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
- package/src/__tests__/history-repair-hook.test.ts +228 -3
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-browser-proxy.test.ts +254 -9
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/image-recovery-hook.test.ts +387 -0
- package/src/__tests__/injector-chain.test.ts +5 -4
- package/src/__tests__/injector-v3-suppression.test.ts +373 -47
- package/src/__tests__/intent-routing.test.ts +7 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/oauth-store.test.ts +0 -85
- package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
- package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
- package/src/__tests__/persist-unsendable-image.test.ts +4 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-bootstrap.test.ts +82 -73
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -8
- package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
- package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
- package/src/__tests__/registry.test.ts +240 -1
- package/src/__tests__/require-fresh-approval.test.ts +3 -0
- package/src/__tests__/schedule-routes.test.ts +116 -1
- package/src/__tests__/schedule-store.test.ts +28 -0
- package/src/__tests__/schedule-tools.test.ts +94 -1
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
- package/src/__tests__/skills.test.ts +202 -0
- package/src/__tests__/slim-skill-category.test.ts +195 -0
- package/src/__tests__/strip-memory-injections.test.ts +33 -39
- package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
- package/src/__tests__/title-generate-hook.test.ts +9 -7
- package/src/__tests__/tool-audit-listener.test.ts +264 -1
- package/src/__tests__/tool-error-hook.test.ts +4 -3
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/tool-start-timestamp.test.ts +218 -0
- package/src/__tests__/tools-get-route.test.ts +202 -0
- package/src/__tests__/workspace-tool-loader.test.ts +319 -0
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
- package/src/agent/loop.ts +569 -319
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/index.ts +10 -0
- package/src/api/responses/conversation-message.ts +135 -27
- package/src/api/responses/memory-v3-selection-log.ts +4 -4
- package/src/approvals/guardian-request-resolvers.ts +26 -0
- package/src/browser-session/backends/host-bridge.ts +29 -0
- package/src/browser-session/index.ts +1 -0
- package/src/browser-session/types.ts +5 -1
- package/src/cli/commands/__tests__/schedules.test.ts +62 -4
- package/src/cli/commands/__tests__/skills.test.ts +53 -0
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/inference-providers.ts +0 -8
- package/src/cli/commands/plugins.ts +2 -2
- package/src/cli/commands/schedules.ts +27 -4
- package/src/cli/commands/skills.ts +187 -146
- package/src/cli/commands/tools.ts +106 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
- package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
- package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
- package/src/cli/lib/install-from-github.ts +187 -117
- package/src/cli/lib/plugin-catalog-cache.ts +9 -9
- package/src/cli/lib/plugin-details.ts +38 -68
- package/src/cli/lib/plugin-marketplace.ts +42 -14
- package/src/cli/lib/search-plugins.ts +29 -129
- package/src/cli/program.ts +2 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
- package/src/config/bundled-skills/app-control/SKILL.md +1 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
- package/src/config/bundled-skills/contacts/SKILL.md +1 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
- package/src/config/bundled-skills/followups/SKILL.md +1 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
- package/src/config/bundled-skills/messaging/SKILL.md +1 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
- package/src/config/bundled-skills/sequences/SKILL.md +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +1 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
- package/src/config/bundled-skills/subagent/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
- package/src/config/env-registry.ts +23 -0
- package/src/config/feature-flag-registry.json +17 -81
- package/src/config/loader.ts +5 -22
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
- package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
- package/src/config/schemas/compaction-logs.ts +79 -0
- package/src/config/schemas/memory-v2.ts +0 -8
- package/src/config/schemas/memory-v3.ts +104 -33
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/config/skills.ts +117 -47
- package/src/context/compactor.ts +11 -0
- package/src/context/strip-injections.ts +38 -4
- package/src/credential-execution/feature-gates.ts +0 -21
- package/src/credential-execution/startup-timeout.ts +32 -4
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
- package/src/daemon/conversation-agent-loop.ts +123 -658
- package/src/daemon/conversation-error.ts +6 -33
- package/src/daemon/conversation-lifecycle.ts +1 -1
- package/src/daemon/conversation-runtime-assembly.ts +183 -22
- package/src/daemon/conversation-skill-tools.ts +137 -8
- package/src/daemon/conversation-slash.ts +0 -14
- package/src/daemon/conversation-store.ts +2 -19
- package/src/daemon/conversation-tool-setup.ts +87 -1
- package/src/daemon/conversation.ts +119 -50
- package/src/daemon/external-plugins-bootstrap.ts +36 -95
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +10 -4
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-browser-proxy.ts +117 -22
- package/src/daemon/lifecycle.ts +34 -5
- package/src/daemon/message-protocol.ts +0 -7
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/skills.ts +17 -0
- package/src/daemon/providers-setup.ts +3 -0
- package/src/daemon/server.ts +3 -3
- package/src/daemon/tool-setup-types.ts +9 -3
- package/src/daemon/trust-context.ts +23 -0
- package/src/daemon/workspace-tools-watcher.ts +324 -0
- package/src/events/tool-audit-listener.ts +78 -16
- package/src/events/tool-metrics-listener.ts +2 -5
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
- package/src/memory/__tests__/conversation-queries.test.ts +176 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
- package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
- package/src/memory/conversation-crud.ts +134 -3
- package/src/memory/conversation-queries.ts +64 -7
- package/src/memory/db-init.ts +14 -0
- package/src/memory/embedding-backend.test.ts +130 -1
- package/src/memory/embedding-backend.ts +79 -106
- package/src/memory/embedding-gemini.ts +5 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
- package/src/memory/graph/conversation-graph-memory.ts +36 -25
- package/src/memory/graph/tool-handlers.ts +3 -0
- package/src/memory/job-handlers/cleanup.ts +3 -1
- package/src/memory/jobs-store.ts +0 -28
- package/src/memory/jobs-worker.ts +10 -22
- package/src/memory/memory-marker.ts +29 -0
- package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
- package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
- package/src/memory/migrations/270-schedule-description.ts +36 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
- package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
- package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
- package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
- package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
- package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +16 -0
- package/src/memory/schema/infrastructure.ts +26 -0
- package/src/memory/skill-loaded-events-store.test.ts +160 -0
- package/src/memory/skill-loaded-events-store.ts +95 -0
- package/src/memory/tool-executed-events-store.test.ts +219 -0
- package/src/memory/tool-executed-events-store.ts +102 -0
- package/src/memory/tool-usage-store.ts +15 -3
- package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
- package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
- package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
- package/src/memory/v2/__tests__/page-store.test.ts +33 -0
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
- package/src/memory/v2/activation-store.ts +50 -1
- package/src/memory/v2/consolidation-job.ts +113 -29
- package/src/memory/v2/injected-block-slugs.ts +79 -0
- package/src/memory/v2/injection.ts +6 -1
- package/src/memory/v2/prompts/consolidation.ts +414 -13
- package/src/memory/v2/router.ts +2 -28
- package/src/memory/v2/static-context.ts +1 -1
- package/src/memory/v2/types.ts +16 -0
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +2 -1
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/oauth/oauth-store.ts +0 -9
- package/src/permissions/checker.test.ts +83 -1
- package/src/permissions/checker.ts +25 -2
- package/src/permissions/gateway-threshold-reader.test.ts +182 -0
- package/src/permissions/gateway-threshold-reader.ts +80 -0
- package/src/platform/client.ts +1 -3
- package/src/platform/feature-gate.ts +3 -12
- package/src/plugin-api/constants.ts +4 -2
- package/src/plugin-api/index.ts +63 -11
- package/src/plugin-api/types.ts +236 -71
- package/src/plugins/defaults/compaction/compact.ts +66 -2
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +303 -1
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
- package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
- package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
- package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
- package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
- package/src/plugins/defaults/history-repair/terminal.ts +39 -2
- package/src/plugins/defaults/image-recovery/detect.ts +25 -0
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
- package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
- package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
- package/src/plugins/defaults/image-recovery/package.json +14 -0
- package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
- package/src/plugins/defaults/index.ts +71 -5
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
- package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
- package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
- package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
- package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
- package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
- package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
- package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
- package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
- package/src/plugins/pipeline.ts +8 -5
- package/src/plugins/types.ts +5 -51
- package/src/providers/cache-control.ts +26 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
- package/src/providers/model-catalog.ts +13 -1
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
- package/src/providers/openai/chat-completions-provider.ts +46 -0
- package/src/providers/openai/responses-provider.ts +45 -0
- package/src/providers/registry.ts +0 -8
- package/src/runtime/__tests__/agent-wake.test.ts +0 -1
- package/src/runtime/agent-wake.ts +13 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
- package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
- package/src/runtime/routes/acp-routes.test.ts +106 -0
- package/src/runtime/routes/acp-routes.ts +248 -2
- package/src/runtime/routes/browser-tabs-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
- package/src/runtime/routes/consolidation-routes.ts +6 -82
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +71 -0
- package/src/runtime/routes/identity-routes.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
- package/src/runtime/routes/plugins-routes.ts +34 -45
- package/src/runtime/routes/schedule-routes.ts +43 -5
- package/src/runtime/routes/settings-routes.ts +140 -15
- package/src/runtime/routes/skills-routes.ts +18 -6
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
- package/src/runtime/services/conversation-serializer.ts +38 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/schedule/schedule-store.ts +19 -0
- package/src/skills/catalog-install.ts +77 -13
- package/src/tasks/task-scheduler.ts +1 -0
- package/src/telemetry/types.ts +66 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
- package/src/telemetry/usage-telemetry-reporter.ts +213 -20
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
- package/src/tools/browser/browser-execution.ts +27 -9
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
- package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
- package/src/tools/browser/cdp-client/factory.ts +217 -17
- package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
- package/src/tools/browser/cdp-client/types.ts +22 -2
- package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
- package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
- package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
- package/src/tools/executor.ts +39 -7
- package/src/tools/registry.ts +387 -5
- package/src/tools/schedule/create.ts +16 -0
- package/src/tools/schedule/list.ts +12 -4
- package/src/tools/schedule/update.ts +12 -0
- package/src/tools/skills/load.ts +11 -6
- package/src/tools/terminal/safe-env.ts +2 -0
- package/src/tools/types.ts +65 -9
- package/src/tools/workspace-tools/loader.ts +673 -0
- package/src/usage/attribution.ts +28 -0
- package/src/util/device-id.ts +17 -3
- package/src/util/platform.ts +16 -0
- package/tsconfig.plugin-api.json +13 -0
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
- package/src/daemon/message-types/browser.ts +0 -10
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
- package/src/plugins/external-api.ts +0 -114
- package/src/plugins/plugin-skill-contributions.ts +0 -292
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace tool loader — discovers and registers tool overrides from
|
|
3
|
+
* `<workspaceDir>/tools/<name>.{ts,js,json}`, plus `<name>.removed`
|
|
4
|
+
* sentinel files that strip a core tool from the registry without
|
|
5
|
+
* substituting a replacement.
|
|
6
|
+
*
|
|
7
|
+
* <workspaceDir>/
|
|
8
|
+
* tools/
|
|
9
|
+
* skill_load.ts ← name="skill_load" via .ts default export
|
|
10
|
+
* my_tool.js ← name="my_tool" via compiled .js (preferred over .ts)
|
|
11
|
+
* data_lookup.json ← name="data_lookup" via JSON spec (no execute)
|
|
12
|
+
* host_bash.removed ← name="host_bash" — strip from registry
|
|
13
|
+
*
|
|
14
|
+
* The filename stem is the registered tool name verbatim — there is no
|
|
15
|
+
* derivation, no basename transformation, no directory layer. This is the
|
|
16
|
+
* documented "directory name is the tool name verbatim" contract, just
|
|
17
|
+
* shifted one level up so each tool is a single file instead of a
|
|
18
|
+
* sub-directory.
|
|
19
|
+
*
|
|
20
|
+
* Override / strip semantics live in the registry
|
|
21
|
+
* ({@link registerWorkspaceTools}, {@link removeCoreToolViaWorkspace}):
|
|
22
|
+
* if a core tool already owns the name, the original is stashed; the
|
|
23
|
+
* workspace tool (or absence-of-tool, for `.removed`) takes its place.
|
|
24
|
+
* Plugins, skills, and MCP servers all refuse to register over a
|
|
25
|
+
* workspace-owned or workspace-stripped name.
|
|
26
|
+
*
|
|
27
|
+
* Lifecycle position:
|
|
28
|
+
*
|
|
29
|
+
* initializeTools()
|
|
30
|
+
* → loadWorkspaceTools() ← this module (first scan)
|
|
31
|
+
* → loadUserPlugins()
|
|
32
|
+
* → bootstrapPlugins()
|
|
33
|
+
* → start file watcher ← hot register/unregister (no restart)
|
|
34
|
+
*
|
|
35
|
+
* Plugins load *after* the initial workspace-tool scan so the registry
|
|
36
|
+
* hands them a stable view of which workspace tools exist before any
|
|
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.
|
|
40
|
+
*
|
|
41
|
+
* Per-tool isolation:
|
|
42
|
+
*
|
|
43
|
+
* - A file with a name that fails {@link isProviderSafeToolName} is
|
|
44
|
+
* logged at error and skipped — we never silently rewrite the name
|
|
45
|
+
* via hashing at registration time (which would mask the operator's
|
|
46
|
+
* typo behind an unfindable hashed identifier).
|
|
47
|
+
* - A `.ts` whose module body throws during import is logged at error
|
|
48
|
+
* and skipped — one broken workspace tool must never block boot.
|
|
49
|
+
* - A `.json` that doesn't parse, or doesn't produce an object, is
|
|
50
|
+
* logged at error and skipped.
|
|
51
|
+
* - A hung dynamic `import()` is bounded by {@link IMPORT_TIMEOUT_MS}
|
|
52
|
+
* and the offending file is skipped. Same contract as the plugin
|
|
53
|
+
* loader: bun's `import()` cannot be cancelled, so an abandoned
|
|
54
|
+
* import keeps running in the background but the registry guards
|
|
55
|
+
* against late-arriving registrations because we already moved past it.
|
|
56
|
+
*
|
|
57
|
+
* The loader registers tools in a single {@link registerWorkspaceTools}
|
|
58
|
+
* call so the batch-level validation can catch duplicate names across
|
|
59
|
+
* the directory listing before any mutation lands.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
63
|
+
import { readFile } from "node:fs/promises";
|
|
64
|
+
import { extname, join } from "node:path";
|
|
65
|
+
import { pathToFileURL } from "node:url";
|
|
66
|
+
|
|
67
|
+
import { getLogger } from "../../util/logger.js";
|
|
68
|
+
import { getWorkspaceToolsDir } from "../../util/platform.js";
|
|
69
|
+
import { isProviderSafeToolName } from "../provider-tool-name.js";
|
|
70
|
+
import { registerWorkspaceTools, removeCoreToolViaWorkspace } from "../registry.js";
|
|
71
|
+
import { finalizeTool } from "../tool-defaults.js";
|
|
72
|
+
import type {
|
|
73
|
+
RiskLevel,
|
|
74
|
+
Tool,
|
|
75
|
+
ToolDefinition,
|
|
76
|
+
ToolExecutionResult,
|
|
77
|
+
} from "../types.js";
|
|
78
|
+
|
|
79
|
+
const log = getLogger("workspace-tool-loader");
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Upper bound on how long a single workspace tool's dynamic `import()` may
|
|
83
|
+
* take. A tool with a hanging top-level `await` (or a never-resolving
|
|
84
|
+
* module evaluation) would otherwise block daemon startup indefinitely,
|
|
85
|
+
* since a raw `try/catch` only isolates thrown errors — not hung
|
|
86
|
+
* promises. Ten seconds matches the plugin-loader budget for the same
|
|
87
|
+
* isolation reason.
|
|
88
|
+
*/
|
|
89
|
+
const IMPORT_TIMEOUT_MS = 10_000;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* File-extension precedence for live tool files. When two files share
|
|
93
|
+
* the same stem (e.g. `tool.ts` + `tool.js`), the loader picks the
|
|
94
|
+
* earlier-listed extension and warns about the rest. Mirrors the
|
|
95
|
+
* external-plugin loader's compiled-binary semantics.
|
|
96
|
+
*/
|
|
97
|
+
const LIVE_TOOL_EXTENSIONS = [".js", ".ts", ".json"] as const;
|
|
98
|
+
type LiveToolExtension = (typeof LIVE_TOOL_EXTENSIONS)[number];
|
|
99
|
+
|
|
100
|
+
/** Extension of the "strip core tool" sentinel. */
|
|
101
|
+
const REMOVED_EXTENSION = ".removed" as const;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Defaults applied when a workspace tool omits one of the normally-required
|
|
105
|
+
* fields. Workspace tools default `defaultRiskLevel` to `"high"` (vs
|
|
106
|
+
* `"medium"` for plugin tools) because they run arbitrary on-disk code
|
|
107
|
+
* under the operator's workspace, so the floor is higher than for
|
|
108
|
+
* in-tree-vetted plugin code.
|
|
109
|
+
*
|
|
110
|
+
* The default `execute` returns an error result so the model sees a clear
|
|
111
|
+
* signal that the tool isn't wired up. The tool still loads cleanly — a
|
|
112
|
+
* broken tool must never block daemon boot.
|
|
113
|
+
*/
|
|
114
|
+
const WORKSPACE_TOOL_DEFAULTS = Object.freeze({
|
|
115
|
+
defaultRiskLevel: "high" as RiskLevel,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate that a filename stem is a usable tool name. Tool names must be
|
|
120
|
+
* provider-safe (`[a-zA-Z0-9_-]{1,64}`) so they can be sent verbatim to
|
|
121
|
+
* the LLM provider without the registry's hash-suffix rewrite kicking in
|
|
122
|
+
* and producing an unfindable name like `my tool__abc123def456`. Names
|
|
123
|
+
* starting with `.` are intentionally excluded so dotfiles (.DS_Store,
|
|
124
|
+
* .gitignore, etc.) cannot accidentally claim a tool.
|
|
125
|
+
*/
|
|
126
|
+
function isValidToolFilenameStem(stem: string): boolean {
|
|
127
|
+
if (stem.length === 0) return false;
|
|
128
|
+
if (stem.startsWith(".")) return false;
|
|
129
|
+
return isProviderSafeToolName(stem);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Strip the recognized extension off a filename and return the stem, or
|
|
134
|
+
* `undefined` if the filename doesn't match any recognized extension.
|
|
135
|
+
*
|
|
136
|
+
* Distinguishes between live tool files (`.ts` / `.js` / `.json`) and the
|
|
137
|
+
* `.removed` sentinel so the caller can route each kind to the right
|
|
138
|
+
* code path.
|
|
139
|
+
*/
|
|
140
|
+
function classifyEntry(
|
|
141
|
+
entry: string,
|
|
142
|
+
): { kind: "live"; stem: string; ext: LiveToolExtension } | { kind: "removed"; stem: string } | undefined {
|
|
143
|
+
const ext = extname(entry);
|
|
144
|
+
if (ext === REMOVED_EXTENSION) {
|
|
145
|
+
return { kind: "removed", stem: entry.slice(0, -REMOVED_EXTENSION.length) };
|
|
146
|
+
}
|
|
147
|
+
for (const candidate of LIVE_TOOL_EXTENSIONS) {
|
|
148
|
+
if (ext === candidate) {
|
|
149
|
+
return { kind: "live", stem: entry.slice(0, -candidate.length), ext: candidate };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* When multiple live files share the same stem (e.g. `foo.ts` + `foo.js`),
|
|
157
|
+
* pick the highest-precedence one per {@link LIVE_TOOL_EXTENSIONS} and
|
|
158
|
+
* return the dropped sibling paths so the caller can warn.
|
|
159
|
+
*/
|
|
160
|
+
interface LiveSelection {
|
|
161
|
+
ext: LiveToolExtension;
|
|
162
|
+
shadowed: LiveToolExtension[];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function selectLiveExtension(extensions: Set<LiveToolExtension>): LiveSelection {
|
|
166
|
+
for (const candidate of LIVE_TOOL_EXTENSIONS) {
|
|
167
|
+
if (extensions.has(candidate)) {
|
|
168
|
+
const shadowed: LiveToolExtension[] = [];
|
|
169
|
+
for (const ext of LIVE_TOOL_EXTENSIONS) {
|
|
170
|
+
if (ext !== candidate && extensions.has(ext)) shadowed.push(ext);
|
|
171
|
+
}
|
|
172
|
+
return { ext: candidate, shadowed };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Unreachable — caller guarantees `extensions` is non-empty.
|
|
176
|
+
throw new Error("selectLiveExtension called with empty set");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Apply workspace-specific defaults on top of the generic
|
|
181
|
+
* {@link finalizeTool} pipeline. The shared finalizer handles the bulk
|
|
182
|
+
* of the omitted-field filling; we layer two workspace-only overrides
|
|
183
|
+
* on top:
|
|
184
|
+
*
|
|
185
|
+
* 1. `defaultRiskLevel` defaults to `"high"` for workspace tools — they
|
|
186
|
+
* run with full user privilege from disk, so the safety floor is set
|
|
187
|
+
* higher than the generic `"medium"` baseline.
|
|
188
|
+
* 2. The default `execute` mentions "workspace tool" so the model gets
|
|
189
|
+
* a clearer "the workspace file at this name has no executor wired
|
|
190
|
+
* up" signal in the failure transcript.
|
|
191
|
+
*
|
|
192
|
+
* The tool still loads cleanly with these defaults — a broken tool must
|
|
193
|
+
* never block daemon boot. Always sets `category: "workspace"` so the
|
|
194
|
+
* registry can distinguish workspace overrides from other origins.
|
|
195
|
+
*/
|
|
196
|
+
function applyWorkspaceToolDefaults(
|
|
197
|
+
tool: ToolDefinition,
|
|
198
|
+
name: string,
|
|
199
|
+
): Tool {
|
|
200
|
+
const finalized = finalizeTool(
|
|
201
|
+
{
|
|
202
|
+
...tool,
|
|
203
|
+
defaultRiskLevel:
|
|
204
|
+
tool.defaultRiskLevel ?? WORKSPACE_TOOL_DEFAULTS.defaultRiskLevel,
|
|
205
|
+
category: tool.category ?? "workspace",
|
|
206
|
+
execute:
|
|
207
|
+
typeof tool.execute === "function"
|
|
208
|
+
? tool.execute
|
|
209
|
+
: async (): Promise<ToolExecutionResult> => ({
|
|
210
|
+
content: `workspace tool ${name} has no execute implementation`,
|
|
211
|
+
isError: true,
|
|
212
|
+
}),
|
|
213
|
+
},
|
|
214
|
+
name,
|
|
215
|
+
);
|
|
216
|
+
return finalized;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Dynamic-import `absolutePath` with a timeout. Resolves to the imported
|
|
221
|
+
* module's default export, or `undefined` if the import times out, has
|
|
222
|
+
* no default export, or throws.
|
|
223
|
+
*
|
|
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.
|
|
228
|
+
*
|
|
229
|
+
* All failure paths log with file attribution so operators can find the
|
|
230
|
+
* broken tool quickly.
|
|
231
|
+
*/
|
|
232
|
+
let importCounter = 0;
|
|
233
|
+
|
|
234
|
+
async function importToolDefaultBounded(
|
|
235
|
+
entryPath: string,
|
|
236
|
+
timeoutMs: number,
|
|
237
|
+
): Promise<unknown> {
|
|
238
|
+
const url = `${pathToFileURL(entryPath).href}?v=${++importCounter}`;
|
|
239
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
240
|
+
try {
|
|
241
|
+
const timeoutSentinel = Symbol("workspace-tool-import-timeout");
|
|
242
|
+
const importPromise = import(url) as Promise<{ default?: unknown }>;
|
|
243
|
+
const timeoutPromise = new Promise<typeof timeoutSentinel>((resolve) => {
|
|
244
|
+
timeoutHandle = setTimeout(() => resolve(timeoutSentinel), timeoutMs);
|
|
245
|
+
});
|
|
246
|
+
const result = await Promise.race([importPromise, timeoutPromise]);
|
|
247
|
+
if (result === timeoutSentinel) {
|
|
248
|
+
// Abandoned import — attach a terminal `.catch` so a late
|
|
249
|
+
// rejection cannot surface as an unhandled-rejection crash.
|
|
250
|
+
importPromise.catch(() => {
|
|
251
|
+
/* swallow — module is dead to the daemon */
|
|
252
|
+
});
|
|
253
|
+
log.warn(
|
|
254
|
+
{ entryPath, timeoutMs },
|
|
255
|
+
`Timed out importing workspace tool from ${entryPath} after ${timeoutMs}ms — skipping`,
|
|
256
|
+
);
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
if (result.default === undefined) {
|
|
260
|
+
log.error(
|
|
261
|
+
{ entryPath },
|
|
262
|
+
`Workspace tool at ${entryPath} has no default export — skipping`,
|
|
263
|
+
);
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return result.default;
|
|
267
|
+
} catch (err) {
|
|
268
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
269
|
+
log.error(
|
|
270
|
+
{ err, entryPath },
|
|
271
|
+
`Failed to import workspace tool from ${entryPath}: ${message}`,
|
|
272
|
+
);
|
|
273
|
+
return undefined;
|
|
274
|
+
} finally {
|
|
275
|
+
if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Read a `.json` tool spec and return its parsed shape, or `undefined`
|
|
281
|
+
* if the file can't be read or doesn't parse to an object.
|
|
282
|
+
*
|
|
283
|
+
* JSON specs can't carry an `execute` function (JSON has no function
|
|
284
|
+
* type), so they always pick up the default error-result executor. They
|
|
285
|
+
* exist for declarative use cases — schema-only tool stubs, override
|
|
286
|
+
* placeholders, etc.
|
|
287
|
+
*/
|
|
288
|
+
async function readJsonToolSpec(entryPath: string): Promise<ToolDefinition | undefined> {
|
|
289
|
+
let raw: string;
|
|
290
|
+
try {
|
|
291
|
+
raw = await readFile(entryPath, "utf8");
|
|
292
|
+
} catch (err) {
|
|
293
|
+
log.error(
|
|
294
|
+
{ err, entryPath },
|
|
295
|
+
`Failed to read JSON workspace tool at ${entryPath} — skipping`,
|
|
296
|
+
);
|
|
297
|
+
return undefined;
|
|
298
|
+
}
|
|
299
|
+
let parsed: unknown;
|
|
300
|
+
try {
|
|
301
|
+
parsed = JSON.parse(raw);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
304
|
+
log.error(
|
|
305
|
+
{ err, entryPath },
|
|
306
|
+
`Failed to parse JSON workspace tool at ${entryPath}: ${message} — skipping`,
|
|
307
|
+
);
|
|
308
|
+
return undefined;
|
|
309
|
+
}
|
|
310
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
311
|
+
log.error(
|
|
312
|
+
{ entryPath, type: typeof parsed },
|
|
313
|
+
`JSON workspace tool at ${entryPath} must be an object — skipping`,
|
|
314
|
+
);
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
return parsed as ToolDefinition;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface LoadWorkspaceToolsOptions {
|
|
321
|
+
/**
|
|
322
|
+
* Maximum time to spend importing each tool file before bailing.
|
|
323
|
+
* Defaults to {@link IMPORT_TIMEOUT_MS}.
|
|
324
|
+
*/
|
|
325
|
+
readonly importTimeoutMs?: number;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
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.
|
|
332
|
+
*/
|
|
333
|
+
export interface LoadWorkspaceToolsResult {
|
|
334
|
+
/** Tool names successfully registered as workspace tools. */
|
|
335
|
+
readonly registered: string[];
|
|
336
|
+
/** Tool names stripped from the registry via `<name>.removed` sentinels. */
|
|
337
|
+
readonly removed: string[];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
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.
|
|
366
|
+
*/
|
|
367
|
+
export async function loadWorkspaceTools(
|
|
368
|
+
options: LoadWorkspaceToolsOptions = {},
|
|
369
|
+
): Promise<LoadWorkspaceToolsResult> {
|
|
370
|
+
const importTimeoutMs = options.importTimeoutMs ?? IMPORT_TIMEOUT_MS;
|
|
371
|
+
const toolsDir = getWorkspaceToolsDir();
|
|
372
|
+
|
|
373
|
+
if (!existsSync(toolsDir)) {
|
|
374
|
+
log.debug({ toolsDir }, "Workspace tools directory does not exist — skipping");
|
|
375
|
+
return { registered: [], removed: [] };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let entries: string[];
|
|
379
|
+
try {
|
|
380
|
+
entries = readdirSync(toolsDir);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
log.warn(
|
|
383
|
+
{ err, toolsDir },
|
|
384
|
+
"loadWorkspaceTools: failed to read tools directory — continuing with no workspace tools",
|
|
385
|
+
);
|
|
386
|
+
return { registered: [], removed: [] };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Group entries by stem so we can detect multi-extension shadowing
|
|
390
|
+
// (e.g. `foo.ts` + `foo.js` claiming the same name) before we kick off
|
|
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>();
|
|
396
|
+
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
const fullPath = join(toolsDir, entry);
|
|
399
|
+
let stats;
|
|
400
|
+
try {
|
|
401
|
+
stats = statSync(fullPath);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
log.warn(
|
|
404
|
+
{ err, fullPath },
|
|
405
|
+
"loadWorkspaceTools: failed to stat candidate — skipping",
|
|
406
|
+
);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (!stats.isFile()) {
|
|
410
|
+
// Subdirectories, symlinks-to-dir, sockets, etc. are silently
|
|
411
|
+
// ignored. The convention is per-tool single files at the top
|
|
412
|
+
// of `<workspaceDir>/tools/`.
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const classified = classifyEntry(entry);
|
|
417
|
+
if (!classified) continue;
|
|
418
|
+
if (!isValidToolFilenameStem(classified.stem)) {
|
|
419
|
+
log.error(
|
|
420
|
+
{ entry, stem: classified.stem, toolsDir },
|
|
421
|
+
`loadWorkspaceTools: filename stem "${classified.stem}" is not a provider-safe tool name (must match /^[a-zA-Z0-9_-]{1,64}$/) — skipping`,
|
|
422
|
+
);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (classified.kind === "removed") {
|
|
427
|
+
removedStems.add(classified.stem);
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
let extensions = liveByStem.get(classified.stem);
|
|
431
|
+
if (!extensions) {
|
|
432
|
+
extensions = new Set<LiveToolExtension>();
|
|
433
|
+
liveByStem.set(classified.stem, extensions);
|
|
434
|
+
}
|
|
435
|
+
extensions.add(classified.ext);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// A stem cannot both be live AND removed. Operator intent is ambiguous;
|
|
439
|
+
// log and skip both entries for that name so neither lands.
|
|
440
|
+
for (const stem of removedStems) {
|
|
441
|
+
if (liveByStem.has(stem)) {
|
|
442
|
+
log.error(
|
|
443
|
+
{ stem, toolsDir },
|
|
444
|
+
`loadWorkspaceTools: "${stem}" has both a live tool file and a .removed sentinel — skipping both (ambiguous intent)`,
|
|
445
|
+
);
|
|
446
|
+
liveByStem.delete(stem);
|
|
447
|
+
removedStems.delete(stem);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
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
|
+
|
|
472
|
+
for (const [stem, extensions] of liveByStem) {
|
|
473
|
+
const { ext: winningExt, shadowed } = selectLiveExtension(extensions);
|
|
474
|
+
if (shadowed.length > 0) {
|
|
475
|
+
log.warn(
|
|
476
|
+
{ stem, winningExt, shadowed, toolsDir },
|
|
477
|
+
`loadWorkspaceTools: "${stem}" has multiple files (${[winningExt, ...shadowed].join(", ")}) — using ${winningExt} and ignoring the rest`,
|
|
478
|
+
);
|
|
479
|
+
}
|
|
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 };
|
|
516
|
+
}
|
|
517
|
+
|
|
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
|
+
}
|
|
539
|
+
}
|
|
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
|
+
|
|
549
|
+
/**
|
|
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.
|
|
556
|
+
*/
|
|
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;
|
|
570
|
+
}
|
|
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;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
let toolSpec: ToolDefinition | undefined;
|
|
580
|
+
if (classified.ext === ".json") {
|
|
581
|
+
toolSpec = await readJsonToolSpec(entryPath);
|
|
582
|
+
} else {
|
|
583
|
+
const defaultExport = await importToolDefaultBounded(entryPath, importTimeoutMs);
|
|
584
|
+
if (defaultExport === undefined) return undefined;
|
|
585
|
+
if (defaultExport === null || typeof defaultExport !== "object") {
|
|
586
|
+
log.error(
|
|
587
|
+
{ entryPath, type: typeof defaultExport },
|
|
588
|
+
`Workspace tool at ${entryPath} default export must be an object — skipping`,
|
|
589
|
+
);
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
toolSpec = defaultExport as ToolDefinition;
|
|
593
|
+
}
|
|
594
|
+
if (!toolSpec) return undefined;
|
|
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
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Classify a single filesystem entry. Exposed for the file watcher so
|
|
612
|
+
* it can route events without re-implementing the extension logic.
|
|
613
|
+
*/
|
|
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
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
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.
|
|
627
|
+
*
|
|
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).
|
|
632
|
+
*/
|
|
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 };
|
|
639
|
+
}
|
|
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 };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const liveExtensions = new Set<LiveToolExtension>();
|
|
652
|
+
let hasRemovedSentinel = false;
|
|
653
|
+
|
|
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);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (liveExtensions.size === 0) {
|
|
665
|
+
return { livePath: null, liveExt: null, hasRemovedSentinel };
|
|
666
|
+
}
|
|
667
|
+
const { ext } = selectLiveExtension(liveExtensions);
|
|
668
|
+
return {
|
|
669
|
+
livePath: join(toolsDir, `${stem}${ext}`),
|
|
670
|
+
liveExt: ext,
|
|
671
|
+
hasRemovedSentinel,
|
|
672
|
+
};
|
|
673
|
+
}
|
package/src/usage/attribution.ts
CHANGED
|
@@ -42,6 +42,34 @@ export interface UsageAttributionSnapshot {
|
|
|
42
42
|
resolvedMixArm: string | null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
/**
|
|
46
|
+
* The four nullable attribution columns shared by telemetry event rows
|
|
47
|
+
* (`tool_invocations`, `skill_loaded_events`).
|
|
48
|
+
*/
|
|
49
|
+
export interface UsageAttributionColumns {
|
|
50
|
+
provider: string | null;
|
|
51
|
+
model: string | null;
|
|
52
|
+
inferenceProfile: string | null;
|
|
53
|
+
inferenceProfileSource: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Maps an attribution snapshot to the shared telemetry columns — the same
|
|
58
|
+
* mapping `llm_usage` reporting uses (`appliedProfile` → inference_profile,
|
|
59
|
+
* `profileSource` → inference_profile_source). Accepts a missing snapshot so
|
|
60
|
+
* producers that resolve attribution best-effort can pass it through as-is.
|
|
61
|
+
*/
|
|
62
|
+
export function toAttributionColumns(
|
|
63
|
+
snapshot: UsageAttributionSnapshot | null | undefined,
|
|
64
|
+
): UsageAttributionColumns {
|
|
65
|
+
return {
|
|
66
|
+
provider: snapshot?.resolvedProvider ?? null,
|
|
67
|
+
model: snapshot?.resolvedModel ?? null,
|
|
68
|
+
inferenceProfile: snapshot?.appliedProfile ?? null,
|
|
69
|
+
inferenceProfileSource: snapshot?.profileSource ?? null,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
/**
|
|
46
74
|
* Sanitizes values before they are copied into external metadata surfaces.
|
|
47
75
|
* Empty strings and control-character-bearing strings are dropped, and long
|