@vellumai/assistant 0.8.8 → 0.8.9-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +6 -6
- package/bun.lock +2 -2
- package/examples/plugins/echo/README.md +61 -60
- package/examples/plugins/echo/hooks/post-tool-use.ts +18 -0
- package/examples/plugins/echo/hooks/stop.ts +16 -0
- package/examples/plugins/echo/hooks/user-prompt-submit.ts +18 -0
- package/examples/plugins/echo/package.json +1 -2
- package/examples/plugins/echo/src/emit.ts +19 -0
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +7 -6
- package/openapi.yaml +235 -6
- package/package.json +2 -2
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +69 -14
- package/src/__tests__/agent-loop-exit-reason.test.ts +204 -144
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +50 -35
- package/src/__tests__/agent-loop-output-hooks.test.ts +357 -0
- package/src/__tests__/agent-loop-override-profile.test.ts +25 -6
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +41 -21
- package/src/__tests__/agent-loop-thinking.test.ts +36 -20
- package/src/__tests__/agent-loop.test.ts +441 -96
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +14 -14
- package/src/__tests__/agent-wake-override-profile.test.ts +17 -21
- package/src/__tests__/anthropic-provider.test.ts +1 -1
- package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
- package/src/__tests__/app-control-flow.test.ts +1 -1
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +132 -0
- package/src/__tests__/approval-cascade.test.ts +5 -4
- package/src/__tests__/approval-routes-http.test.ts +4 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +1 -1
- package/src/__tests__/channel-approvals.test.ts +1 -1
- package/src/__tests__/compaction-circuit.test.ts +258 -0
- package/src/__tests__/compaction-direct.test.ts +132 -0
- package/src/__tests__/compaction-events.test.ts +5 -5
- package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
- package/src/__tests__/context-overflow-reducer.test.ts +1 -1
- package/src/__tests__/conversation-abort-tool-results.test.ts +6 -4
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +7 -10
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +78 -119
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +142 -218
- package/src/__tests__/conversation-agent-loop.test.ts +297 -586
- package/src/__tests__/conversation-clean-command.test.ts +5 -2
- package/src/__tests__/conversation-confirmation-signals.test.ts +5 -4
- package/src/__tests__/conversation-crud-inference-profile.test.ts +7 -9
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +4 -4
- package/src/__tests__/conversation-process-callsite.test.ts +14 -14
- package/src/__tests__/conversation-provider-retry-repair.test.ts +70 -65
- package/src/__tests__/conversation-queue.test.ts +9 -9
- package/src/__tests__/conversation-runtime-assembly.test.ts +923 -231
- package/src/__tests__/conversation-runtime-workspace.test.ts +115 -20
- package/src/__tests__/conversation-slash-queue.test.ts +6 -4
- package/src/__tests__/conversation-slash-unknown.test.ts +6 -4
- package/src/__tests__/conversation-speed-override.test.ts +10 -9
- package/src/__tests__/conversation-starter-routes.test.ts +14 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +23 -20
- package/src/__tests__/conversation-workspace-injection.test.ts +68 -6
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +14 -11
- package/src/__tests__/conversations-import-system-filter.test.ts +101 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-acp-history.test.ts +101 -0
- package/src/__tests__/dynamic-page-surface.test.ts +31 -0
- package/src/__tests__/empty-response-hook.test.ts +1 -1
- package/src/__tests__/file-write-tool.test.ts +63 -0
- package/src/__tests__/gateway-only-guard.test.ts +12 -2
- package/src/__tests__/guardian-grant-minting.test.ts +1 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +2 -4
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -0
- package/src/__tests__/history-repair-hook.test.ts +1 -1
- package/src/__tests__/host-app-control-routes.test.ts +1 -1
- package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
- package/src/__tests__/inference-profile-reaper.test.ts +62 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +86 -0
- package/src/__tests__/injector-background-turn.test.ts +13 -23
- package/src/__tests__/injector-chain.test.ts +268 -44
- package/src/__tests__/injector-disk-pressure.test.ts +210 -52
- package/src/__tests__/injector-document-comments.test.ts +97 -114
- package/src/__tests__/injector-pkb-v2-silenced.test.ts +2 -2
- package/src/__tests__/injector-v3-suppression.test.ts +4 -4
- package/src/__tests__/list-messages-client-message-id.test.ts +91 -0
- package/src/__tests__/list-messages-hidden-metadata.test.ts +38 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +131 -26
- package/src/__tests__/memory-v2-static-injector.test.ts +86 -8
- package/src/__tests__/parallel-tool.benchmark.test.ts +35 -8
- package/src/__tests__/plugin-api-shim.test.ts +6 -9
- package/src/__tests__/plugin-bootstrap.test.ts +12 -23
- package/src/__tests__/plugin-registry.test.ts +3 -49
- package/src/__tests__/plugin-types.test.ts +0 -70
- package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
- package/src/__tests__/reaction-persistence.test.ts +1 -1
- package/src/__tests__/send-endpoint-busy.test.ts +4 -1
- package/src/__tests__/skill-feature-flags-integration.test.ts +33 -0
- package/src/__tests__/steer-tool-repair.test.ts +1 -1
- package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
- package/src/__tests__/subagent-detail.test.ts +25 -7
- package/src/__tests__/subagent-fork-notifications.test.ts +1 -3
- package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
- package/src/__tests__/subagent-manager-notify.test.ts +1 -3
- package/src/__tests__/subagent-notify-parent.test.ts +1 -3
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +1 -1
- package/src/__tests__/tool-error-hook.test.ts +1 -1
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -1
- package/src/__tests__/user-plugin-loader.test.ts +54 -286
- package/src/acp/__tests__/agent-process.test.ts +161 -0
- package/src/acp/__tests__/client-handler.test.ts +40 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +82 -0
- package/src/acp/__tests__/helpers/exec-file-stub.ts +106 -0
- package/src/acp/__tests__/prepare-agent-env.test.ts +97 -0
- package/src/acp/__tests__/session-manager-persistence.test.ts +95 -28
- package/src/acp/__tests__/session-manager-resume.test.ts +888 -0
- package/src/acp/agent-process.ts +61 -1
- package/src/acp/auto-install.test.ts +280 -0
- package/src/acp/auto-install.ts +232 -0
- package/src/acp/client-handler.ts +31 -0
- package/src/acp/feature-gate.test.ts +48 -0
- package/src/acp/feature-gate.ts +34 -0
- package/src/acp/prepare-agent-env.ts +80 -27
- package/src/acp/resolve-agent.test.ts +225 -9
- package/src/acp/resolve-agent.ts +122 -17
- package/src/acp/resume-hint.ts +23 -0
- package/src/acp/session-manager.ts +507 -73
- package/src/agent/compaction-circuit.ts +60 -102
- package/src/agent/loop.ts +414 -248
- package/src/api/responses/conversation-message.ts +14 -1
- package/src/approvals/guardian-request-resolvers.ts +1 -1
- package/src/background-wake/next-wake.ts +1 -0
- package/src/cli/commands/db/__tests__/repair.test.ts +3 -1
- package/src/cli/commands/plugins.ts +43 -37
- package/src/cli/lib/__tests__/install-from-github.test.ts +429 -111
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +196 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +372 -0
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +220 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +226 -32
- package/src/cli/lib/install-from-github.ts +464 -55
- package/src/cli/lib/plugin-catalog-cache.ts +84 -0
- package/src/cli/lib/plugin-details.ts +409 -0
- package/src/cli/lib/plugin-marketplace.ts +197 -0
- package/src/cli/lib/search-plugins.ts +195 -29
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
- package/src/config/acp-defaults.test.ts +10 -0
- package/src/config/acp-defaults.ts +6 -0
- package/src/config/bundled-skills/acp/SKILL.md +85 -33
- package/src/config/bundled-skills/acp/TOOLS.json +4 -4
- package/src/config/bundled-skills/app-builder/SKILL.md +224 -381
- package/src/config/bundled-skills/app-builder/TOOLS.json +72 -2
- package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +48 -0
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +57 -0
- package/src/config/bundled-skills/app-builder/references/SLIDES.md +38 -0
- package/src/config/bundled-skills/app-builder/tools/app-list.ts +62 -0
- package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +28 -23
- package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +15 -6
- package/src/config/schemas/call-site-catalog.ts +0 -14
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm.ts +0 -2
- package/src/context/compactor.ts +22 -4
- package/src/context/strip-injections.ts +8 -2
- package/src/context/window-manager.ts +27 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +10 -35
- package/src/daemon/conversation-agent-loop.ts +175 -1055
- package/src/daemon/conversation-lifecycle.ts +11 -255
- package/src/daemon/conversation-process.ts +8 -136
- package/src/daemon/conversation-registry.ts +159 -0
- package/src/daemon/conversation-runtime-assembly.ts +293 -392
- package/src/daemon/conversation-store.ts +9 -90
- package/src/daemon/conversation-surfaces.ts +24 -8
- package/src/daemon/conversation-workspace.ts +17 -0
- package/src/daemon/conversation.ts +404 -57
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/external-plugins-bootstrap.ts +14 -19
- package/src/daemon/handlers/conversations.ts +3 -1
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-proxy-preactivation.ts +1 -3
- package/src/daemon/lifecycle.ts +21 -7
- package/src/daemon/server.ts +2 -0
- package/src/daemon/wake-conversation-ops.ts +269 -0
- package/src/embedded/plugin-api.ts +2 -2
- package/src/export/__tests__/transcript-formatter.test.ts +5 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +3 -0
- package/src/heartbeat/heartbeat-run-store.ts +23 -1
- package/src/heartbeat/heartbeat-service.ts +26 -0
- package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
- package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
- package/src/ipc/skill-routes/__tests__/memory.test.ts +15 -0
- package/src/ipc/skill-routes/memory.ts +4 -2
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +87 -0
- package/src/memory/conversation-crud.ts +29 -19
- package/src/memory/conversation-starter-checkpoints.ts +1 -0
- package/src/memory/db-init.ts +2 -0
- package/src/memory/job-handlers/conversation-starters.ts +13 -2
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +5 -4
- package/src/memory/jobs-worker.ts +25 -1
- package/src/memory/migrations/272-acp-session-history-cwd.ts +36 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema/acp.ts +4 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
- package/src/memory/v2/consolidation-job.ts +13 -4
- package/src/plugin-api/constants.ts +4 -0
- package/src/plugin-api/index.ts +6 -5
- package/src/plugin-api/types.ts +75 -0
- package/src/plugins/defaults/compaction/compact.ts +59 -0
- package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
- package/src/plugins/defaults/compaction/manager-store.ts +57 -0
- package/src/plugins/defaults/compaction/package.json +1 -2
- package/src/plugins/defaults/empty-response/package.json +0 -1
- package/src/plugins/defaults/history-repair/package.json +0 -1
- package/src/plugins/defaults/index.ts +135 -26
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +100 -52
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +184 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
- package/src/plugins/defaults/{injectors/register.ts → memory-retrieval/injectors.ts} +148 -73
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +223 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/assign.test.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/live-integration.test.ts +9 -6
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/maintain-job.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/orchestrate.test.ts +8 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/reconcile.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/render-injection.test.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/router.test.ts +10 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selection-log-store.test.ts +8 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/selector.test.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/shadow-plugin.test.ts +16 -17
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/types.test.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/assign.ts +9 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/capabilities.ts +5 -2
- package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +14 -0
- package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +75 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/maintain-job.ts +15 -8
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/orchestrate.ts +2 -2
- package/src/plugins/defaults/memory-v3-shadow/package.json +14 -0
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/page-content.ts +2 -2
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/provider-blocks.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/reconcile.ts +7 -3
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/render-injection.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/router.ts +5 -5
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selection-log-store.ts +4 -4
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/selector.ts +7 -7
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/shadow-plugin.ts +32 -94
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/tree.ts +1 -1
- package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/types.ts +1 -1
- package/src/plugins/defaults/title-generate/package.json +0 -1
- package/src/plugins/defaults/tool-error/package.json +0 -1
- package/src/plugins/defaults/tool-result-truncate/package.json +0 -1
- package/src/plugins/pipeline.ts +6 -293
- package/src/plugins/registry.ts +9 -37
- package/src/plugins/types.ts +76 -381
- package/src/plugins/user-loader.ts +30 -127
- package/src/prompts/__tests__/system-prompt.test.ts +6 -0
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +35 -3
- package/src/runtime/__tests__/agent-wake.test.ts +555 -691
- package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
- package/src/runtime/agent-wake.ts +108 -209
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/channel-approvals.ts +1 -1
- package/src/runtime/interactive-ui.ts +1 -1
- package/src/runtime/routes/__tests__/acp-routes.test.ts +315 -55
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +423 -73
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +5 -4
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +4 -1
- package/src/runtime/routes/acp-routes.test.ts +89 -25
- package/src/runtime/routes/acp-routes.ts +81 -29
- package/src/runtime/routes/approval-routes.ts +1 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/browser-tabs-routes.ts +6 -10
- package/src/runtime/routes/conversation-cli-routes.ts +1 -1
- package/src/runtime/routes/conversation-list-routes.ts +1 -1
- package/src/runtime/routes/conversation-query-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +28 -2
- package/src/runtime/routes/conversation-starter-routes.ts +13 -7
- package/src/runtime/routes/conversations-import-routes.ts +24 -7
- package/src/runtime/routes/host-app-control-routes.ts +1 -1
- package/src/runtime/routes/host-cu-routes.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +18 -3
- package/src/runtime/routes/inbound-message-handler.ts +1 -1
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -0
- package/src/runtime/routes/inference-profile-session-reaper.ts +6 -0
- package/src/runtime/routes/memory-v3-routes.ts +16 -6
- package/src/runtime/routes/playground/helpers.ts +1 -1
- package/src/runtime/routes/plugins-routes.ts +337 -35
- package/src/runtime/routes/surface-conversation-resolver.ts +4 -3
- package/src/runtime/routes/work-items-routes.ts +2 -4
- package/src/runtime/services/conversation-serializer.ts +1 -1
- package/src/signals/cancel.ts +2 -4
- package/src/subagent/manager.ts +21 -5
- package/src/tools/acp/context.ts +20 -0
- package/src/tools/acp/list-agents.test.ts +8 -2
- package/src/tools/acp/spawn.test.ts +176 -195
- package/src/tools/acp/spawn.ts +37 -172
- package/src/tools/acp/steer.test.ts +105 -8
- package/src/tools/acp/steer.ts +48 -17
- package/src/tools/apps/executors.ts +166 -50
- package/src/tools/filesystem/write.ts +34 -0
- package/src/tools/subagent/spawn.ts +2 -4
- package/src/tools/ui-surface/definitions.ts +25 -5
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +4 -5
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +69 -45
- package/docs/plugins.md +0 -832
- package/examples/plugins/echo/register.ts +0 -143
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -405
- package/src/__tests__/compaction-pipeline.test.ts +0 -210
- package/src/__tests__/compaction-timeout-recovery.test.ts +0 -251
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -667
- package/src/__tests__/pipeline-runner.test.ts +0 -554
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/daemon/wake-target-adapter.ts +0 -253
- package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +0 -93
- package/src/plugins/defaults/circuit-breaker/package.json +0 -15
- package/src/plugins/defaults/circuit-breaker/register.ts +0 -39
- package/src/plugins/defaults/compaction/middlewares/compaction.ts +0 -25
- package/src/plugins/defaults/compaction/register.ts +0 -35
- package/src/plugins/defaults/compaction/terminal.ts +0 -73
- package/src/plugins/defaults/empty-response/register.ts +0 -23
- package/src/plugins/defaults/history-repair/register.ts +0 -24
- package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +0 -126
- package/src/plugins/defaults/overflow-reduce/package.json +0 -15
- package/src/plugins/defaults/overflow-reduce/register.ts +0 -42
- package/src/plugins/defaults/title-generate/register.ts +0 -35
- package/src/plugins/defaults/tool-error/register.ts +0 -23
- package/src/plugins/defaults/tool-result-truncate/register.ts +0 -24
- package/src/plugins/external-api.ts +0 -104
- package/src/proactive-artifact/aux-message-injector.ts +0 -97
- package/src/proactive-artifact/decision.test.ts +0 -226
- package/src/proactive-artifact/decision.ts +0 -165
- package/src/proactive-artifact/index.ts +0 -7
- package/src/proactive-artifact/job.test.ts +0 -962
- package/src/proactive-artifact/job.ts +0 -372
- package/src/proactive-artifact/message-copy.ts +0 -58
- package/src/proactive-artifact/trigger-state.test.ts +0 -286
- package/src/proactive-artifact/trigger-state.ts +0 -123
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/capabilities.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/core.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/eval-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/fixtures/live-turns.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/health.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/needle.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/provider-blocks.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/snapshot.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/tree.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-eviction.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/__tests__/working-set-skeleton.test.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/core.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/README.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/assignments.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/core.json +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-x.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-a/topic-y.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/data/leaves/domain-b/topic-z.md +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/health.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/llm-retry.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/needle.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/snapshot.ts +0 -0
- /package/src/{memory/v3 → plugins/defaults/memory-v3-shadow}/working-set.ts +0 -0
package/src/acp/agent-process.ts
CHANGED
|
@@ -34,6 +34,7 @@ export type AcpClientFactory = (agent: Agent) => Client;
|
|
|
34
34
|
export class AcpAgentProcess {
|
|
35
35
|
private proc: ChildProcess | null = null;
|
|
36
36
|
private connection: acp.ClientSideConnection | null = null;
|
|
37
|
+
private initializeResponse: InitializeResponse | null = null;
|
|
37
38
|
|
|
38
39
|
constructor(
|
|
39
40
|
public readonly agentId: string,
|
|
@@ -99,7 +100,7 @@ export class AcpAgentProcess {
|
|
|
99
100
|
|
|
100
101
|
log.info({ agentId: this.agentId }, "Initializing ACP connection");
|
|
101
102
|
|
|
102
|
-
|
|
103
|
+
const response = await this.connection.initialize({
|
|
103
104
|
protocolVersion: acp.PROTOCOL_VERSION,
|
|
104
105
|
clientInfo: { name: "vellum", version: "1.0.0" },
|
|
105
106
|
clientCapabilities: {
|
|
@@ -107,6 +108,28 @@ export class AcpAgentProcess {
|
|
|
107
108
|
terminal: true,
|
|
108
109
|
},
|
|
109
110
|
});
|
|
111
|
+
|
|
112
|
+
this.initializeResponse = response;
|
|
113
|
+
return response;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether the agent advertised support for `session/load` at initialize.
|
|
118
|
+
* Returns false before initialize() resolves.
|
|
119
|
+
*/
|
|
120
|
+
get supportsLoadSession(): boolean {
|
|
121
|
+
return this.initializeResponse?.agentCapabilities?.loadSession === true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether the agent advertised support for `session/resume` at initialize.
|
|
126
|
+
* Returns false before initialize() resolves.
|
|
127
|
+
*/
|
|
128
|
+
get supportsSessionResume(): boolean {
|
|
129
|
+
return (
|
|
130
|
+
this.initializeResponse?.agentCapabilities?.sessionCapabilities?.resume !=
|
|
131
|
+
null
|
|
132
|
+
);
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
/**
|
|
@@ -128,6 +151,41 @@ export class AcpAgentProcess {
|
|
|
128
151
|
return result.sessionId;
|
|
129
152
|
}
|
|
130
153
|
|
|
154
|
+
/**
|
|
155
|
+
* Loads a previously persisted ACP session via `session/load`.
|
|
156
|
+
*
|
|
157
|
+
* Per the ACP spec, the agent replays the session's conversation history
|
|
158
|
+
* as `session/update` notifications before the load response resolves;
|
|
159
|
+
* callers should suppress forwarding of those replayed updates (see
|
|
160
|
+
* VellumAcpClientHandler.beginReplaySuppression).
|
|
161
|
+
*/
|
|
162
|
+
async loadSession(sessionId: string, cwd: string): Promise<void> {
|
|
163
|
+
if (!this.connection) {
|
|
164
|
+
throw new Error(`ACP agent "${this.agentId}" is not spawned`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
log.info({ agentId: this.agentId, sessionId, cwd }, "Loading ACP session");
|
|
168
|
+
|
|
169
|
+
await this.connection.loadSession({ sessionId, cwd, mcpServers: [] });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Resumes a previously persisted ACP session via `session/resume`.
|
|
174
|
+
*
|
|
175
|
+
* Unlike `session/load`, resume performs no history replay, so it is
|
|
176
|
+
* preferred when the agent advertises the capability
|
|
177
|
+
* (see supportsSessionResume).
|
|
178
|
+
*/
|
|
179
|
+
async resumeSession(sessionId: string, cwd: string): Promise<void> {
|
|
180
|
+
if (!this.connection) {
|
|
181
|
+
throw new Error(`ACP agent "${this.agentId}" is not spawned`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
log.info({ agentId: this.agentId, sessionId, cwd }, "Resuming ACP session");
|
|
185
|
+
|
|
186
|
+
await this.connection.resumeSession({ sessionId, cwd, mcpServers: [] });
|
|
187
|
+
}
|
|
188
|
+
|
|
131
189
|
/**
|
|
132
190
|
* Sends a prompt to an existing ACP session.
|
|
133
191
|
* Returns the prompt response (includes stopReason).
|
|
@@ -175,6 +233,7 @@ export class AcpAgentProcess {
|
|
|
175
233
|
this.proc = null;
|
|
176
234
|
}
|
|
177
235
|
this.connection = null;
|
|
236
|
+
this.initializeResponse = null;
|
|
178
237
|
}
|
|
179
238
|
|
|
180
239
|
/**
|
|
@@ -205,5 +264,6 @@ export class AcpAgentProcess {
|
|
|
205
264
|
|
|
206
265
|
this.proc = null;
|
|
207
266
|
this.connection = null;
|
|
267
|
+
this.initializeResponse = null;
|
|
208
268
|
}
|
|
209
269
|
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the sandboxed ACP adapter auto-installer.
|
|
3
|
+
*
|
|
4
|
+
* `execFile` is stubbed via the shared `installExecFileStub` helper: a
|
|
5
|
+
* process-global `mock.module("node:child_process", ...)` driven by per-call
|
|
6
|
+
* scripted responses keyed on `${command} ${args[0]}`. `Bun.which` is stubbed
|
|
7
|
+
* via `installWhichStub` so each test controls whether `bun` is on PATH and
|
|
8
|
+
* whether the adapter binary resolves after the install.
|
|
9
|
+
*
|
|
10
|
+
* The security-critical assertions live here: the installer must be `bun`
|
|
11
|
+
* (never `npm`), run in a fresh temp dir (NOT the task cwd), with the ACP
|
|
12
|
+
* secrets stripped from its env and the registry pinned to the public npm
|
|
13
|
+
* registry.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
18
|
+
|
|
19
|
+
import { installAcpConfigStub } from "./__tests__/helpers/acp-config-stub.js";
|
|
20
|
+
import { installExecFileStub } from "./__tests__/helpers/exec-file-stub.js";
|
|
21
|
+
import { installWhichStub } from "./__tests__/helpers/which-stub.js";
|
|
22
|
+
|
|
23
|
+
const { execScripts, execFileMock, reset } = installExecFileStub();
|
|
24
|
+
const config = await installAcpConfigStub();
|
|
25
|
+
const which = installWhichStub();
|
|
26
|
+
|
|
27
|
+
/** Fixed resolved `bun` path so script keys are predictable. */
|
|
28
|
+
const BUN_BIN = "/usr/local/bin/bun";
|
|
29
|
+
/** Key the exec stub uses for the global install. */
|
|
30
|
+
const BUN_ADD_KEY = `${BUN_BIN} add`;
|
|
31
|
+
|
|
32
|
+
afterAll(() => {
|
|
33
|
+
which.restore();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Spread the real module so other test files that load logger consumers
|
|
37
|
+
// (e.g. `truncateForLog` importers) after this process-global mock still
|
|
38
|
+
// resolve every named export.
|
|
39
|
+
const realLogger = await import("../util/logger.js");
|
|
40
|
+
mock.module("../util/logger.js", () => ({
|
|
41
|
+
...realLogger,
|
|
42
|
+
getLogger: () =>
|
|
43
|
+
new Proxy({} as Record<string, unknown>, {
|
|
44
|
+
get: () => () => {},
|
|
45
|
+
}),
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
ensureAdapterInstalled,
|
|
50
|
+
resolveAgentWithAutoInstall,
|
|
51
|
+
_resetAdapterInstallCacheForTests,
|
|
52
|
+
} = await import("./auto-install.js");
|
|
53
|
+
|
|
54
|
+
/** Latest call's execFile options ({ cwd, env, ... }). */
|
|
55
|
+
function lastInstallOptions(): { cwd?: string; env?: NodeJS.ProcessEnv } {
|
|
56
|
+
const call = execFileMock.mock.calls.at(-1)!;
|
|
57
|
+
return call[2] as { cwd?: string; env?: NodeJS.ProcessEnv };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
reset();
|
|
62
|
+
_resetAdapterInstallCacheForTests();
|
|
63
|
+
config.setConfig({ agents: {} });
|
|
64
|
+
// Default: bun on PATH, nothing else.
|
|
65
|
+
which.setWhich({ bun: BUN_BIN });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("ensureAdapterInstalled", () => {
|
|
69
|
+
test("known command: runs `bun add --global <pkg>` and reports installed", async () => {
|
|
70
|
+
execScripts.set(BUN_ADD_KEY, { stdout: "" });
|
|
71
|
+
|
|
72
|
+
const result = await ensureAdapterInstalled("claude-agent-acp");
|
|
73
|
+
|
|
74
|
+
expect(result).toEqual({ installed: true });
|
|
75
|
+
expect(execFileMock).toHaveBeenCalledTimes(1);
|
|
76
|
+
const [command, args] = execFileMock.mock.calls[0];
|
|
77
|
+
expect(command).toBe(BUN_BIN);
|
|
78
|
+
expect(args).toEqual([
|
|
79
|
+
"add",
|
|
80
|
+
"--global",
|
|
81
|
+
"@agentclientprotocol/claude-agent-acp",
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("installer never invokes npm", async () => {
|
|
86
|
+
execScripts.set(BUN_ADD_KEY, { stdout: "" });
|
|
87
|
+
|
|
88
|
+
await ensureAdapterInstalled("claude-agent-acp");
|
|
89
|
+
|
|
90
|
+
for (const call of execFileMock.mock.calls) {
|
|
91
|
+
expect(call[0]).not.toBe("npm");
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("runs in a fresh temp dir (NOT the task cwd), token-free env, public registry", async () => {
|
|
96
|
+
// Seed the secrets on the ambient env so we can assert they are stripped.
|
|
97
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = "should-not-leak";
|
|
98
|
+
process.env.GEMINI_API_KEY = "should-not-leak-either";
|
|
99
|
+
execScripts.set(BUN_ADD_KEY, { stdout: "" });
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await ensureAdapterInstalled("claude-agent-acp");
|
|
103
|
+
} finally {
|
|
104
|
+
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
105
|
+
delete process.env.GEMINI_API_KEY;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const { cwd, env } = lastInstallOptions();
|
|
109
|
+
// Sandboxed cwd: a temp dir under the OS temp root, never the process cwd.
|
|
110
|
+
expect(cwd).toBeDefined();
|
|
111
|
+
expect(cwd!.startsWith(tmpdir())).toBe(true);
|
|
112
|
+
expect(cwd).not.toBe(process.cwd());
|
|
113
|
+
expect(cwd).toContain("vellum-acp-install-");
|
|
114
|
+
|
|
115
|
+
// Secrets stripped, registry pinned.
|
|
116
|
+
expect(env).toBeDefined();
|
|
117
|
+
expect(env!.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined();
|
|
118
|
+
expect(env!.GEMINI_API_KEY).toBeUndefined();
|
|
119
|
+
expect(env!.BUN_CONFIG_REGISTRY).toBe("https://registry.npmjs.org/");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("unknown command: never invokes a package manager (security allowlist)", async () => {
|
|
123
|
+
const result = await ensureAdapterInstalled("some-arbitrary-binary");
|
|
124
|
+
|
|
125
|
+
expect(result.installed).toBe(false);
|
|
126
|
+
expect(result.error).toBeUndefined();
|
|
127
|
+
expect(execFileMock).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("bun absent: no install attempted, reports not installed", async () => {
|
|
131
|
+
which.setWhich({}); // bun not on PATH
|
|
132
|
+
|
|
133
|
+
const result = await ensureAdapterInstalled("claude-agent-acp");
|
|
134
|
+
|
|
135
|
+
expect(result.installed).toBe(false);
|
|
136
|
+
expect(result.error).toBeUndefined();
|
|
137
|
+
expect(execFileMock).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("install failure: reports the error and does not install", async () => {
|
|
141
|
+
execScripts.set(BUN_ADD_KEY, {
|
|
142
|
+
error: new Error("EACCES: permission denied"),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const result = await ensureAdapterInstalled("codex-acp");
|
|
146
|
+
|
|
147
|
+
expect(result.installed).toBe(false);
|
|
148
|
+
expect(result.error).toContain("EACCES");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("failed install is retried on the next call", async () => {
|
|
152
|
+
execScripts.set(BUN_ADD_KEY, { error: new Error("network down") });
|
|
153
|
+
const first = await ensureAdapterInstalled("claude-agent-acp");
|
|
154
|
+
expect(first.installed).toBe(false);
|
|
155
|
+
|
|
156
|
+
execScripts.set(BUN_ADD_KEY, { stdout: "" });
|
|
157
|
+
const second = await ensureAdapterInstalled("claude-agent-acp");
|
|
158
|
+
expect(second.installed).toBe(true);
|
|
159
|
+
expect(execFileMock).toHaveBeenCalledTimes(2);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("successful install is cached for the process lifetime", async () => {
|
|
163
|
+
execScripts.set(BUN_ADD_KEY, { stdout: "" });
|
|
164
|
+
|
|
165
|
+
await ensureAdapterInstalled("claude-agent-acp");
|
|
166
|
+
await ensureAdapterInstalled("claude-agent-acp");
|
|
167
|
+
|
|
168
|
+
expect(execFileMock).toHaveBeenCalledTimes(1);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("concurrent calls dedupe to exactly one install", async () => {
|
|
172
|
+
execScripts.set(BUN_ADD_KEY, { stdout: "" });
|
|
173
|
+
|
|
174
|
+
const [a, b, c] = await Promise.all([
|
|
175
|
+
ensureAdapterInstalled("claude-agent-acp"),
|
|
176
|
+
ensureAdapterInstalled("claude-agent-acp"),
|
|
177
|
+
ensureAdapterInstalled("claude-agent-acp"),
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
expect(a.installed).toBe(true);
|
|
181
|
+
expect(b.installed).toBe(true);
|
|
182
|
+
expect(c.installed).toBe(true);
|
|
183
|
+
expect(execFileMock).toHaveBeenCalledTimes(1);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("different commands install independently", async () => {
|
|
187
|
+
execScripts.set(BUN_ADD_KEY, { stdout: "" });
|
|
188
|
+
|
|
189
|
+
const [claude, codex] = await Promise.all([
|
|
190
|
+
ensureAdapterInstalled("claude-agent-acp"),
|
|
191
|
+
ensureAdapterInstalled("codex-acp"),
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
expect(claude.installed).toBe(true);
|
|
195
|
+
expect(codex.installed).toBe(true);
|
|
196
|
+
expect(execFileMock).toHaveBeenCalledTimes(2);
|
|
197
|
+
const installedPackages = execFileMock.mock.calls.map(
|
|
198
|
+
(call) => (call[1] as string[])[2],
|
|
199
|
+
);
|
|
200
|
+
expect(installedPackages.sort()).toEqual([
|
|
201
|
+
"@agentclientprotocol/claude-agent-acp",
|
|
202
|
+
"@zed-industries/codex-acp",
|
|
203
|
+
]);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("resolveAgentWithAutoInstall - resolution order", () => {
|
|
208
|
+
test("binary missing + bun present: installs then resolves to the real binary", async () => {
|
|
209
|
+
let installed = false;
|
|
210
|
+
which.setWhich((cmd) => {
|
|
211
|
+
if (cmd === "bun") return BUN_BIN;
|
|
212
|
+
if (cmd === "claude-agent-acp" && installed) {
|
|
213
|
+
return "/usr/local/bin/claude-agent-acp";
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
});
|
|
217
|
+
execScripts.set(BUN_ADD_KEY, {
|
|
218
|
+
stdout: "",
|
|
219
|
+
onCall: () => {
|
|
220
|
+
installed = true;
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const result = await resolveAgentWithAutoInstall("claude");
|
|
225
|
+
|
|
226
|
+
expect(result.resolved.ok).toBe(true);
|
|
227
|
+
if (!result.resolved.ok) return;
|
|
228
|
+
// The resolved command is the REAL binary, not a `bun x` wrapper.
|
|
229
|
+
expect(result.resolved.agent.command).toBe("claude-agent-acp");
|
|
230
|
+
expect(result.autoInstalledPackage).toBe(
|
|
231
|
+
"@agentclientprotocol/claude-agent-acp",
|
|
232
|
+
);
|
|
233
|
+
expect(result.failureMessage).toBeUndefined();
|
|
234
|
+
expect(execFileMock).toHaveBeenCalledTimes(1);
|
|
235
|
+
expect(execFileMock.mock.calls[0][0]).toBe(BUN_BIN);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("binary missing + bun absent: no install, plain failure with the hint", async () => {
|
|
239
|
+
which.setWhich({}); // neither bun nor the adapter on PATH
|
|
240
|
+
|
|
241
|
+
const result = await resolveAgentWithAutoInstall("claude");
|
|
242
|
+
|
|
243
|
+
expect(result.resolved.ok).toBe(false);
|
|
244
|
+
expect(result.autoInstalledPackage).toBeUndefined();
|
|
245
|
+
// No augmented failure message (the plain binary_not_found hint is
|
|
246
|
+
// surfaced by the caller via formatResolveFailure).
|
|
247
|
+
expect(result.failureMessage).toBeUndefined();
|
|
248
|
+
expect(execFileMock).not.toHaveBeenCalled();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("install fails: failure message carries the hint and install error, never npm", async () => {
|
|
252
|
+
which.setWhich({ bun: BUN_BIN });
|
|
253
|
+
execScripts.set(BUN_ADD_KEY, { error: new Error("network is down") });
|
|
254
|
+
|
|
255
|
+
const result = await resolveAgentWithAutoInstall("claude");
|
|
256
|
+
|
|
257
|
+
expect(result.resolved.ok).toBe(false);
|
|
258
|
+
expect(result.failureMessage).toContain("claude-agent-acp is not on PATH");
|
|
259
|
+
expect(result.failureMessage).toContain(
|
|
260
|
+
"bun add -g @agentclientprotocol/claude-agent-acp",
|
|
261
|
+
);
|
|
262
|
+
expect(result.failureMessage).toContain("network is down");
|
|
263
|
+
for (const call of execFileMock.mock.calls) {
|
|
264
|
+
expect(call[0]).not.toBe("npm");
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("codex unaffected: already on PATH resolves with no install", async () => {
|
|
269
|
+
which.setWhich((cmd) =>
|
|
270
|
+
cmd === "codex-acp" ? "/usr/local/bin/codex-acp" : null,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const result = await resolveAgentWithAutoInstall("codex");
|
|
274
|
+
|
|
275
|
+
expect(result.resolved.ok).toBe(true);
|
|
276
|
+
if (!result.resolved.ok) return;
|
|
277
|
+
expect(result.resolved.agent.command).toBe("codex-acp");
|
|
278
|
+
expect(execFileMock).not.toHaveBeenCalled();
|
|
279
|
+
});
|
|
280
|
+
});
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandboxed auto-install for known ACP adapter binaries.
|
|
3
|
+
*
|
|
4
|
+
* When a spawn fails preflight with `binary_not_found`, callers (the
|
|
5
|
+
* `acp_spawn` tool and the `/v1/acp/spawn` route) call
|
|
6
|
+
* `resolveAgentWithAutoInstall(agentId)`, which performs a ONE-TIME global
|
|
7
|
+
* install of the mapped adapter package via `bun`, then re-resolves and
|
|
8
|
+
* continues. After the install the adapter is a normal trusted binary on
|
|
9
|
+
* PATH, and the session manager spawns it the usual way (project cwd, token
|
|
10
|
+
* injected only at spawn).
|
|
11
|
+
*
|
|
12
|
+
* Security boundaries (this is the ATL-808 fix):
|
|
13
|
+
* - Only commands present in `DEFAULT_AGENT_NPM_PACKAGES` are ever
|
|
14
|
+
* installed. The package names are vendored constants, NOT user input.
|
|
15
|
+
* - The install runs `bun` with cwd = a FRESH empty daemon-owned temp dir,
|
|
16
|
+
* never the untrusted task project dir. A clean cwd has no project-local
|
|
17
|
+
* `node_modules/.bin`, `bunfig.toml`, or `.npmrc`, so none of the
|
|
18
|
+
* cwd-based package-resolution hijacks apply.
|
|
19
|
+
* - The installer env is a SANITIZED copy of `process.env` with
|
|
20
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` and `GEMINI_API_KEY` stripped, so the secret
|
|
21
|
+
* is never in scope during package resolution, and `BUN_CONFIG_REGISTRY`
|
|
22
|
+
* forced to the public npm registry so a redirected registry cannot serve
|
|
23
|
+
* a malicious package.
|
|
24
|
+
* - The token is injected ONLY later, at spawn time, on the real installed
|
|
25
|
+
* binary (see `prepare-agent-env.ts`). `prepareAgentEnv` is never called
|
|
26
|
+
* here.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { execFile } from "node:child_process";
|
|
30
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
|
|
34
|
+
import { DEFAULT_AGENT_NPM_PACKAGES } from "../config/acp-defaults.js";
|
|
35
|
+
import { getLogger } from "../util/logger.js";
|
|
36
|
+
import {
|
|
37
|
+
resolveAcpAgent,
|
|
38
|
+
type ResolveAcpAgentResult,
|
|
39
|
+
} from "./resolve-agent.js";
|
|
40
|
+
|
|
41
|
+
const log = getLogger("acp:auto-install");
|
|
42
|
+
|
|
43
|
+
/** Per-install timeout for the global install. Generous: cold caches are slow. */
|
|
44
|
+
const BUN_INSTALL_TIMEOUT_MS = 120_000;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The trusted public npm registry. Forced via `BUN_CONFIG_REGISTRY` on the
|
|
48
|
+
* installer env so a stray `.npmrc`/`bunfig.toml` in the ambient environment
|
|
49
|
+
* cannot redirect package downloads to an attacker-controlled registry.
|
|
50
|
+
*/
|
|
51
|
+
const PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
52
|
+
|
|
53
|
+
export interface AdapterInstallResult {
|
|
54
|
+
installed: boolean;
|
|
55
|
+
error?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Per-command install promises. Concurrent spawns for the same missing
|
|
60
|
+
* binary dedupe to a single global install; successful results are cached for
|
|
61
|
+
* the process lifetime. Failed installs are evicted so a later spawn can
|
|
62
|
+
* retry (e.g. after the user fixes network or install permissions).
|
|
63
|
+
*/
|
|
64
|
+
const installPromises = new Map<string, Promise<AdapterInstallResult>>();
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Run `execFile` with an AbortController-driven timeout. Returns the stdout
|
|
68
|
+
* on success; throws on error or timeout. The optional `cwd`/`env` let the
|
|
69
|
+
* installer run in a sandboxed working directory with a sanitized env.
|
|
70
|
+
*/
|
|
71
|
+
function execFileWithTimeout(
|
|
72
|
+
command: string,
|
|
73
|
+
args: string[],
|
|
74
|
+
timeoutMs: number,
|
|
75
|
+
options?: { cwd?: string; env?: NodeJS.ProcessEnv },
|
|
76
|
+
): Promise<string> {
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
80
|
+
execFile(
|
|
81
|
+
command,
|
|
82
|
+
args,
|
|
83
|
+
{
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
cwd: options?.cwd,
|
|
87
|
+
env: options?.env,
|
|
88
|
+
},
|
|
89
|
+
(err, stdout) => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (err) {
|
|
92
|
+
reject(err);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
resolve(stdout);
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A copy of `process.env` safe to hand to the package installer: the ACP
|
|
103
|
+
* secrets are stripped so they can never leak into a resolved package's
|
|
104
|
+
* lifecycle/runtime, and the registry is pinned to the trusted public one.
|
|
105
|
+
*/
|
|
106
|
+
function sanitizedInstallEnv(): NodeJS.ProcessEnv {
|
|
107
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
108
|
+
delete env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
109
|
+
delete env.GEMINI_API_KEY;
|
|
110
|
+
env.BUN_CONFIG_REGISTRY = PUBLIC_NPM_REGISTRY;
|
|
111
|
+
return env;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Install the npm-registry package mapped to `command` via a sandboxed `bun`
|
|
116
|
+
* global install, if (and only if) the command is a known adapter binary and
|
|
117
|
+
* `bun` is on PATH. Unknown commands, or hosts without `bun`, resolve to
|
|
118
|
+
* `{ installed: false }` without ever invoking a package manager (see the
|
|
119
|
+
* security boundary note in the module doc).
|
|
120
|
+
*/
|
|
121
|
+
export function ensureAdapterInstalled(
|
|
122
|
+
command: string,
|
|
123
|
+
): Promise<AdapterInstallResult> {
|
|
124
|
+
const packageName = DEFAULT_AGENT_NPM_PACKAGES[command];
|
|
125
|
+
if (!packageName) {
|
|
126
|
+
return Promise.resolve({ installed: false });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const bunPath = Bun.which("bun");
|
|
130
|
+
if (!bunPath) {
|
|
131
|
+
return Promise.resolve({ installed: false });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const inFlight = installPromises.get(command);
|
|
135
|
+
if (inFlight) return inFlight;
|
|
136
|
+
|
|
137
|
+
const promise = runInstall(bunPath, command, packageName).then((result) => {
|
|
138
|
+
if (!result.installed) installPromises.delete(command);
|
|
139
|
+
return result;
|
|
140
|
+
});
|
|
141
|
+
installPromises.set(command, promise);
|
|
142
|
+
return promise;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runInstall(
|
|
146
|
+
bunPath: string,
|
|
147
|
+
command: string,
|
|
148
|
+
packageName: string,
|
|
149
|
+
): Promise<AdapterInstallResult> {
|
|
150
|
+
log.info({ command, packageName }, "Auto-installing missing ACP adapter");
|
|
151
|
+
// Fresh empty dir guaranteed to have no project-local node_modules,
|
|
152
|
+
// bunfig.toml, or .npmrc - this neutralizes the cwd-based resolution
|
|
153
|
+
// hijacks the untrusted task dir would otherwise enable.
|
|
154
|
+
const installDir = await mkdtemp(join(tmpdir(), "vellum-acp-install-"));
|
|
155
|
+
try {
|
|
156
|
+
await execFileWithTimeout(
|
|
157
|
+
bunPath,
|
|
158
|
+
// `bun add --global` installs AND links the package bin into bun's
|
|
159
|
+
// global bin dir (on PATH in every image).
|
|
160
|
+
["add", "--global", packageName],
|
|
161
|
+
BUN_INSTALL_TIMEOUT_MS,
|
|
162
|
+
{ cwd: installDir, env: sanitizedInstallEnv() },
|
|
163
|
+
);
|
|
164
|
+
log.info({ command, packageName }, "ACP adapter auto-install succeeded");
|
|
165
|
+
return { installed: true };
|
|
166
|
+
} catch (err) {
|
|
167
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
168
|
+
log.warn(
|
|
169
|
+
{ err, command, packageName },
|
|
170
|
+
"ACP adapter auto-install failed (falling back to install hint)",
|
|
171
|
+
);
|
|
172
|
+
return { installed: false, error };
|
|
173
|
+
} finally {
|
|
174
|
+
await rm(installDir, { recursive: true, force: true }).catch(() => {});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface ResolveWithAutoInstallResult {
|
|
179
|
+
/** The final resolver outcome (post-install re-resolve when applicable). */
|
|
180
|
+
resolved: ResolveAcpAgentResult;
|
|
181
|
+
/** Set when a missing adapter binary was silently installed via bun. */
|
|
182
|
+
autoInstalledPackage?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Set when the auto-install itself failed: the original install hint
|
|
185
|
+
* augmented with the install failure reason. Callers should surface this
|
|
186
|
+
* instead of re-deriving a message from `resolved`.
|
|
187
|
+
*/
|
|
188
|
+
failureMessage?: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolve an ACP agent id, silently auto-installing the mapped adapter
|
|
193
|
+
* package when (and only when) the failure is a missing allowlisted binary.
|
|
194
|
+
* Shared by the `acp_spawn` tool and the `/v1/acp/spawn` route so the
|
|
195
|
+
* resolve-install-re-resolve flow has a single implementation; callers map
|
|
196
|
+
* the result to their transport (tool error result vs. HTTP error class).
|
|
197
|
+
*/
|
|
198
|
+
export async function resolveAgentWithAutoInstall(
|
|
199
|
+
agentId: string,
|
|
200
|
+
): Promise<ResolveWithAutoInstallResult> {
|
|
201
|
+
const resolved = resolveAcpAgent(agentId);
|
|
202
|
+
if (resolved.ok || resolved.reason !== "binary_not_found") {
|
|
203
|
+
return { resolved };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const { command, hint } = resolved;
|
|
207
|
+
const install = await ensureAdapterInstalled(command);
|
|
208
|
+
if (install.installed) {
|
|
209
|
+
const retried = resolveAcpAgent(agentId);
|
|
210
|
+
if (retried.ok) {
|
|
211
|
+
log.info(
|
|
212
|
+
{ agentId, command },
|
|
213
|
+
"Auto-installed missing ACP adapter binary",
|
|
214
|
+
);
|
|
215
|
+
return {
|
|
216
|
+
resolved: retried,
|
|
217
|
+
autoInstalledPackage: DEFAULT_AGENT_NPM_PACKAGES[command],
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
} else if (install.error) {
|
|
221
|
+
return {
|
|
222
|
+
resolved,
|
|
223
|
+
failureMessage: `${command} is not on PATH. ${hint} (auto-install failed: ${install.error})`,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return { resolved };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** @internal: exposed for tests only. */
|
|
230
|
+
export function _resetAdapterInstallCacheForTests(): void {
|
|
231
|
+
installPromises.clear();
|
|
232
|
+
}
|
|
@@ -51,6 +51,7 @@ interface TerminalState {
|
|
|
51
51
|
export class VellumAcpClientHandler implements Client {
|
|
52
52
|
private terminals = new Map<string, TerminalState>();
|
|
53
53
|
private accumulatedText = "";
|
|
54
|
+
private suppressForwarding = false;
|
|
54
55
|
/** Tracks pending ACP permission requestIds for cleanup on session close. */
|
|
55
56
|
readonly pendingRequestIds = new Set<string>();
|
|
56
57
|
|
|
@@ -65,8 +66,38 @@ export class VellumAcpClientHandler implements Client {
|
|
|
65
66
|
private readonly parentConversationId: string,
|
|
66
67
|
) {}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Begins suppressing session updates from being forwarded to Vellum.
|
|
71
|
+
*
|
|
72
|
+
* Per the ACP spec, `session/load` replays the entire conversation history
|
|
73
|
+
* as `session/update` notifications before the load response resolves. The
|
|
74
|
+
* parent conversation already received those events during the original
|
|
75
|
+
* run, so re-forwarding them would duplicate them into the conversation and
|
|
76
|
+
* the ring buffer. Callers wrap `loadSession` in
|
|
77
|
+
* beginReplaySuppression()/endReplaySuppression() to drop the replay.
|
|
78
|
+
* `session/resume` performs no replay, which is why it is preferred when
|
|
79
|
+
* the agent supports it.
|
|
80
|
+
*/
|
|
81
|
+
beginReplaySuppression(): void {
|
|
82
|
+
this.suppressForwarding = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Ends replay suppression; subsequent updates flow normally. */
|
|
86
|
+
endReplaySuppression(): void {
|
|
87
|
+
this.suppressForwarding = false;
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
async sessionUpdate(params: SessionNotification): Promise<void> {
|
|
69
91
|
const update = params.update;
|
|
92
|
+
|
|
93
|
+
if (this.suppressForwarding) {
|
|
94
|
+
log.debug(
|
|
95
|
+
{ acpSessionId: this.acpSessionId, updateType: update.sessionUpdate },
|
|
96
|
+
"Dropping replayed session update during suppression",
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
70
101
|
log.debug(
|
|
71
102
|
{ acpSessionId: this.acpSessionId, updateType: update.sessionUpdate },
|
|
72
103
|
"ACP session update received",
|