@vellumai/assistant 0.10.1 → 0.10.2-dev.202606241651.2d2b40d
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/docs/workspace-tools.md +42 -33
- package/eslint-rules/cli-no-daemon-internals.js +6 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
- package/openapi.yaml +74 -1
- package/package.json +1 -1
- package/scripts/test.sh +36 -15
- package/src/__tests__/actor-token-service.test.ts +36 -14
- package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
- package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
- package/src/__tests__/annotate-risk-options.test.ts +2 -0
- package/src/__tests__/approval-cascade.test.ts +2 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
- package/src/__tests__/btw-routes.test.ts +2 -0
- package/src/__tests__/build-persisted-content.test.ts +2 -0
- package/src/__tests__/call-controller.test.ts +19 -0
- package/src/__tests__/channel-guardian.test.ts +94 -58
- package/src/__tests__/channel-reply-delivery.test.ts +2 -0
- package/src/__tests__/compaction-events.test.ts +2 -0
- package/src/__tests__/compaction.benchmark.test.ts +2 -0
- package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
- package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
- package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
- package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
- package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
- package/src/__tests__/computer-use-tools.test.ts +13 -0
- package/src/__tests__/config-loader-backfill.test.ts +5 -1
- package/src/__tests__/config-schema.test.ts +1 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
- package/src/__tests__/contacts-relay-reads.test.ts +13 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +7 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
- package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
- package/src/__tests__/conversation-history-web-search.test.ts +2 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
- package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +2 -0
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
- package/src/__tests__/conversation-process-callsite.test.ts +2 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
- package/src/__tests__/conversation-queue.test.ts +91 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
- package/src/__tests__/conversation-slash-queue.test.ts +2 -0
- package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
- package/src/__tests__/conversation-speed-override.test.ts +2 -0
- package/src/__tests__/conversation-surfaces-action-delivery.test.ts +65 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -0
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
- package/src/__tests__/conversation-usage.test.ts +2 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
- package/src/__tests__/credential-security-invariants.test.ts +0 -1
- package/src/__tests__/db-migration-rollback.test.ts +205 -171
- package/src/__tests__/db-test-helpers.ts +5 -4
- package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
- package/src/__tests__/disk-pressure-guard.test.ts +41 -0
- package/src/__tests__/dm-persistence.test.ts +2 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +10 -5
- package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
- package/src/__tests__/filing-service.test.ts +2 -0
- package/src/__tests__/guardian-binding-drift-heal.test.ts +75 -10
- package/src/__tests__/guardian-dispatch.test.ts +95 -1
- package/src/__tests__/guardian-outbound-http.test.ts +13 -0
- package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
- package/src/__tests__/heartbeat-service.test.ts +2 -0
- package/src/__tests__/helpers/channel-test-adapter.ts +1 -7
- package/src/__tests__/host-app-control-routes.test.ts +24 -30
- package/src/__tests__/host-bash-routes.test.ts +31 -41
- package/src/__tests__/host-browser-routes.test.ts +26 -32
- package/src/__tests__/host-cu-proxy.test.ts +299 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
- package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
- package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
- package/src/__tests__/http-user-message-parity.test.ts +167 -8
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/invite-redemption-service.test.ts +43 -0
- package/src/__tests__/llm-context-normalization.test.ts +105 -0
- package/src/__tests__/llm-usage-store.test.ts +25 -0
- package/src/__tests__/media-stream-server-integration.test.ts +127 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
- package/src/__tests__/messaging-send-tool.test.ts +2 -0
- package/src/__tests__/migration-import-from-url.test.ts +2 -2
- package/src/__tests__/native-web-search.test.ts +2 -0
- package/src/__tests__/non-member-access-request.test.ts +189 -17
- package/src/__tests__/notification-broadcaster.test.ts +4 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
- package/src/__tests__/notification-deep-link.test.ts +6 -0
- package/src/__tests__/notification-guardian-path.test.ts +19 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
- package/src/__tests__/plugin-bootstrap.test.ts +3 -73
- package/src/__tests__/plugin-route-contribution.test.ts +4 -17
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -18
- package/src/__tests__/plugin-types.test.ts +0 -2
- package/src/__tests__/process-message-background-slack.test.ts +2 -0
- package/src/__tests__/process-message-display-content.test.ts +2 -0
- package/src/__tests__/provider-usage-tracking.test.ts +39 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
- package/src/__tests__/registry.test.ts +3 -0
- package/src/__tests__/relay-server.test.ts +694 -25
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/secret-ingress-http.test.ts +14 -0
- package/src/__tests__/send-endpoint-busy.test.ts +30 -8
- package/src/__tests__/skills.test.ts +44 -0
- package/src/__tests__/slack-inbound-verification.test.ts +47 -2
- package/src/__tests__/sse-actor-principal-guardian-source.test.ts +102 -0
- package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
- package/src/__tests__/stt-hints.test.ts +44 -13
- package/src/__tests__/subagent-detail.test.ts +27 -0
- package/src/__tests__/subagent-disposal.test.ts +65 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
- package/src/__tests__/subagent-tools.test.ts +2 -0
- package/src/__tests__/suggestion-routes.test.ts +2 -0
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
- package/src/__tests__/tool-executor.test.ts +16 -11
- package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
- package/src/__tests__/tool-start-timestamp.test.ts +2 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +10 -10
- package/src/__tests__/twilio-routes.test.ts +96 -0
- package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
- package/src/__tests__/web-search-backend-failure.test.ts +2 -0
- package/src/__tests__/workspace-tool-loader.test.ts +195 -2
- package/src/agent/loop-exclusive-tool.test.ts +150 -0
- package/src/agent/loop.ts +56 -0
- package/src/api/constants/sse-replay.ts +41 -0
- package/src/api/index.ts +6 -0
- package/src/api/responses/llm-request-log-entry.ts +25 -0
- package/src/api/responses/subagent-detail.ts +17 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +262 -4
- package/src/calls/call-domain.ts +3 -3
- package/src/calls/guardian-dispatch.ts +10 -8
- package/src/calls/inbound-trust-reader.ts +17 -1
- package/src/calls/media-stream-server.ts +21 -0
- package/src/calls/relay-server.ts +167 -50
- package/src/calls/relay-setup-router.ts +37 -7
- package/src/calls/relay-verification.ts +4 -4
- package/src/calls/stt-hints.ts +9 -12
- package/src/calls/twilio-routes.ts +14 -4
- package/src/cli/commands/__tests__/cache.test.ts +8 -1
- package/src/cli/commands/cache.ts +194 -181
- package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
- package/src/cli/commands/db/status.ts +37 -1
- package/src/cli/commands/mcp.ts +252 -218
- package/src/cli/commands/memory/__tests__/worker.test.ts +302 -0
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/worker.ts +175 -0
- package/src/cli/commands/plugins.ts +75 -3
- package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
- package/src/cli/lib/list-installed-plugins.ts +179 -1
- package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +6 -1
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
- package/src/config/feature-flag-registry.json +0 -8
- package/src/config/loader.ts +36 -5
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/memory-v3.ts +7 -0
- package/src/config/schemas/memory.ts +4 -0
- package/src/config/schemas/timeouts.ts +8 -0
- package/src/config/seed-inference-profiles.ts +14 -5
- package/src/config/skills.ts +27 -5
- package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
- package/src/contacts/contacts-write.ts +3 -0
- package/src/contacts/guardian-delivery-reader.ts +223 -0
- package/src/daemon/conversation-agent-loop.ts +9 -0
- package/src/daemon/conversation-process.ts +39 -17
- package/src/daemon/conversation-surfaces.ts +8 -0
- package/src/daemon/conversation-tool-setup.ts +49 -16
- package/src/daemon/conversation.ts +21 -2
- package/src/daemon/disk-pressure-guard.ts +12 -2
- package/src/daemon/event-loop-watchdog.ts +28 -1
- package/src/daemon/external-plugins-bootstrap.ts +4 -34
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -0
- package/src/daemon/handlers/__tests__/config-channels.test.ts +225 -0
- package/src/daemon/handlers/config-a2a.ts +6 -14
- package/src/daemon/handlers/config-channels.ts +78 -22
- package/src/daemon/handlers/conversations.ts +77 -0
- package/src/daemon/host-cu-proxy.ts +102 -11
- package/src/daemon/lifecycle.ts +4 -0
- package/src/daemon/memory-v2-startup.test.ts +72 -0
- package/src/daemon/memory-v2-startup.ts +87 -19
- package/src/daemon/server.ts +0 -4
- package/src/daemon/shutdown-handlers.ts +20 -0
- package/src/daemon/tool-setup-types.ts +9 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
- package/src/ipc/assistant-server.ts +2 -2
- package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
- package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
- package/src/memory/__tests__/prompt-override.test.ts +192 -0
- package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
- package/src/memory/conversation-crud.ts +38 -0
- package/src/memory/db-connection.ts +22 -3
- package/src/memory/db-init.ts +36 -502
- package/src/memory/db-singleton.ts +6 -4
- package/src/memory/jobs-worker.ts +58 -0
- package/src/memory/llm-usage-store.ts +48 -20
- package/src/memory/memory-retrospective-job.ts +9 -8
- package/src/memory/migrations/014-backfill-inbox-thread-state.ts +13 -3
- package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -27
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +130 -56
- package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
- package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
- package/src/memory/migrations/__tests__/014-backfill-inbox-thread-state.test.ts +108 -0
- package/src/memory/migrations/__tests__/136-drop-assistant-id-columns.test.ts +82 -0
- package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +224 -0
- package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
- package/src/memory/migrations/run-migrations.ts +90 -6
- package/src/memory/migrations/schema-introspection.ts +14 -0
- package/src/memory/migrations/validate-migration-state.ts +101 -66
- package/src/memory/prompt-override.ts +129 -0
- package/src/memory/schema/conversations.ts +9 -0
- package/src/memory/schema/infrastructure.ts +20 -0
- package/src/memory/steps.ts +573 -0
- package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
- package/src/memory/v2/cli-command-store.ts +75 -38
- package/src/memory/v2/prompts/consolidation.ts +13 -82
- package/src/memory/v2/prompts/router.ts +21 -93
- package/src/memory/v2/skill-store.ts +68 -31
- package/src/memory/watchdog-events-store.ts +87 -0
- package/src/memory/worker-control.ts +118 -0
- package/src/memory/worker-process.ts +72 -0
- package/src/notifications/__tests__/broadcaster.test.ts +16 -8
- package/src/notifications/__tests__/connected-channels.test.ts +114 -0
- package/src/notifications/__tests__/decision-engine.test.ts +78 -9
- package/src/notifications/__tests__/destination-resolver.test.ts +256 -0
- package/src/notifications/broadcaster.ts +8 -1
- package/src/notifications/decision-engine.ts +15 -7
- package/src/notifications/destination-resolver.ts +68 -24
- package/src/notifications/emit-signal.ts +39 -14
- package/src/onboarding/checkin-event.test.ts +220 -0
- package/src/onboarding/checkin-event.ts +321 -0
- package/src/onboarding/schedule-checkin.ts +190 -0
- package/src/permissions/question-prompter.test.ts +1 -1
- package/src/permissions/question-prompter.ts +7 -4
- package/src/plugin-api/index.ts +6 -6
- package/src/plugin-api/types.ts +3 -5
- package/src/plugin-api/vision-support.test.ts +28 -4
- package/src/plugin-api/vision-support.ts +66 -31
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +161 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +106 -0
- package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +60 -0
- package/src/plugins/defaults/advisor/consult.ts +110 -6
- package/src/plugins/defaults/advisor/context-pack.ts +288 -0
- package/src/plugins/defaults/advisor/steering.ts +14 -2
- package/src/plugins/defaults/advisor/tools/advisor.ts +32 -5
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
- package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +10 -11
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +12 -20
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +29 -1
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +8 -1
- package/src/plugins/mtime-cache.ts +7 -2
- package/src/plugins/types.ts +0 -2
- package/src/providers/anthropic/client.ts +5 -0
- package/src/providers/call-site-routing.ts +4 -0
- package/src/providers/model-catalog.ts +16 -0
- package/src/providers/openai/responses-provider.ts +5 -0
- package/src/providers/openrouter/client.ts +5 -0
- package/src/providers/provider-send-message.ts +4 -0
- package/src/providers/ratelimit.ts +4 -0
- package/src/providers/retry.ts +4 -0
- package/src/providers/types.ts +9 -0
- package/src/providers/usage-tracking.ts +4 -0
- package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
- package/src/runtime/__tests__/guardian-vellum-migration.test.ts +181 -0
- package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
- package/src/runtime/__tests__/local-principal-trust.test.ts +164 -0
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +335 -3
- package/src/runtime/access-request-helper.ts +19 -39
- package/src/runtime/actor-trust-resolver.ts +2 -2
- package/src/runtime/anchored-guardian.test.ts +156 -0
- package/src/runtime/anchored-guardian.ts +135 -0
- package/src/runtime/assistant-event-hub.ts +1 -1
- package/src/runtime/assistant-stream-state.ts +9 -2
- package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
- package/src/runtime/auth/require-bound-guardian.ts +21 -11
- package/src/runtime/channel-verification-service.ts +56 -31
- package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
- package/src/runtime/guardian-vellum-migration.ts +66 -7
- package/src/runtime/invite-redemption-service.ts +50 -18
- package/src/runtime/local-actor-identity.ts +76 -11
- package/src/runtime/local-principal-trust.ts +52 -0
- package/src/runtime/pending-interactions.ts +11 -1
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
- package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/contact-routes.test.ts +212 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +93 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +215 -1
- package/src/runtime/routes/browser-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +3 -3
- package/src/runtime/routes/contact-routes.ts +8 -32
- package/src/runtime/routes/conversation-cli-routes.ts +4 -5
- package/src/runtime/routes/conversation-list-routes.ts +4 -7
- package/src/runtime/routes/conversation-routes.ts +74 -81
- package/src/runtime/routes/events-routes.ts +2 -2
- package/src/runtime/routes/global-search-routes.ts +3 -1
- package/src/runtime/routes/guardian-action-routes.ts +4 -5
- package/src/runtime/routes/host-app-control-routes.ts +5 -4
- package/src/runtime/routes/host-bash-routes.ts +5 -4
- package/src/runtime/routes/host-browser-routes.ts +9 -11
- package/src/runtime/routes/host-cu-routes.ts +5 -4
- package/src/runtime/routes/host-file-routes.ts +5 -4
- package/src/runtime/routes/host-transfer-routes.ts +6 -6
- package/src/runtime/routes/http-adapter.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +3 -2
- package/src/runtime/routes/inbound-message-handler.ts +5 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -49
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +16 -4
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/llm-context-normalization.ts +71 -0
- package/src/runtime/routes/mcp-auth-routes.ts +38 -15
- package/src/runtime/routes/migration-rollback-routes.ts +4 -3
- package/src/runtime/routes/migration-routes.ts +4 -1
- package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
- package/src/runtime/routes/subagents-routes.ts +5 -0
- package/src/runtime/routes/surface-action-routes.ts +51 -55
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
- package/src/runtime/services/conversation-serializer.ts +7 -9
- package/src/runtime/tool-grant-request-helper.ts +3 -3
- package/src/runtime/trust-verdict-consumer.ts +85 -9
- package/src/runtime/verification-outbound-actions.ts +18 -18
- package/src/signals/user-message.ts +16 -0
- package/src/subagent/manager.ts +9 -0
- package/src/telemetry/types.ts +34 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
- package/src/telemetry/usage-telemetry-reporter.ts +87 -3
- package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
- package/src/tools/ask-question/ask-question-tool.ts +13 -0
- package/src/tools/computer-use/definitions.ts +8 -2
- package/src/tools/executor.ts +4 -4
- package/src/tools/registry.ts +18 -0
- package/src/tools/tool-approval-handler.ts +1 -1
- package/src/tools/tool-defaults.ts +9 -2
- package/src/tools/types.ts +17 -2
- package/src/tools/workspace-tools/loader.ts +348 -244
- package/src/util/platform.ts +5 -0
- package/src/util/telemetry-db-path.ts +24 -0
- package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
- package/src/daemon/workspace-tools-watcher.ts +0 -328
- package/src/memory/migrations/registry.ts +0 -573
|
@@ -95,7 +95,7 @@ afterAll(() => {
|
|
|
95
95
|
|
|
96
96
|
const handleHostBashResult = ROUTES.find(
|
|
97
97
|
(r) => r.endpoint === "host-bash-result",
|
|
98
|
-
)!.handler
|
|
98
|
+
)!.handler as (args: Record<string, unknown>) => Promise<unknown>;
|
|
99
99
|
|
|
100
100
|
// ── Helpers ──────────────────────────────────────────────────────────
|
|
101
101
|
|
|
@@ -244,22 +244,20 @@ describe("handleHostBashResult", () => {
|
|
|
244
244
|
).toThrow(ForbiddenError);
|
|
245
245
|
});
|
|
246
246
|
|
|
247
|
-
test("interaction is NOT resolved on cross-actor 403 (still pending)", () => {
|
|
247
|
+
test("interaction is NOT resolved on cross-actor 403 (still pending)", async () => {
|
|
248
248
|
const requestId = "req-actor-mismatch-stays";
|
|
249
249
|
clientActorPrincipals.set("client-abc", "principal-victim");
|
|
250
250
|
registerPending(requestId, { targetClientId: "client-abc" });
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
});
|
|
260
|
-
} catch {
|
|
252
|
+
await handleHostBashResult({
|
|
253
|
+
body: bashBody(requestId),
|
|
254
|
+
headers: {
|
|
255
|
+
"x-vellum-client-id": "client-abc",
|
|
256
|
+
"x-vellum-actor-principal-id": "principal-attacker",
|
|
257
|
+
},
|
|
258
|
+
}).catch(() => {
|
|
261
259
|
// expected
|
|
262
|
-
}
|
|
260
|
+
});
|
|
263
261
|
|
|
264
262
|
expect(resolvedIds).not.toContain(requestId);
|
|
265
263
|
expect(pendingStore.has(requestId)).toBe(true);
|
|
@@ -278,19 +276,17 @@ describe("handleHostBashResult", () => {
|
|
|
278
276
|
).toThrow(ForbiddenError);
|
|
279
277
|
});
|
|
280
278
|
|
|
281
|
-
test("interaction is NOT resolved when submitting actor is missing (still pending)", () => {
|
|
279
|
+
test("interaction is NOT resolved when submitting actor is missing (still pending)", async () => {
|
|
282
280
|
const requestId = "req-actor-missing-stays";
|
|
283
281
|
clientActorPrincipals.set("client-abc", "principal-victim");
|
|
284
282
|
registerPending(requestId, { targetClientId: "client-abc" });
|
|
285
283
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
});
|
|
291
|
-
} catch {
|
|
284
|
+
await handleHostBashResult({
|
|
285
|
+
body: bashBody(requestId),
|
|
286
|
+
headers: { "x-vellum-client-id": "client-abc" },
|
|
287
|
+
}).catch(() => {
|
|
292
288
|
// expected
|
|
293
|
-
}
|
|
289
|
+
});
|
|
294
290
|
|
|
295
291
|
expect(resolvedIds).not.toContain(requestId);
|
|
296
292
|
expect(pendingStore.has(requestId)).toBe(true);
|
|
@@ -358,15 +354,13 @@ describe("handleHostBashResult", () => {
|
|
|
358
354
|
).toThrow(BadRequestError);
|
|
359
355
|
});
|
|
360
356
|
|
|
361
|
-
test("interaction is NOT resolved on 400 (still pending)", () => {
|
|
357
|
+
test("interaction is NOT resolved on 400 (still pending)", async () => {
|
|
362
358
|
const requestId = "req-targeted-no-header-stays";
|
|
363
359
|
registerPending(requestId, { targetClientId: "client-abc" });
|
|
364
360
|
|
|
365
|
-
|
|
366
|
-
handleHostBashResult({ body: bashBody(requestId) });
|
|
367
|
-
} catch {
|
|
361
|
+
await handleHostBashResult({ body: bashBody(requestId) }).catch(() => {
|
|
368
362
|
// expected
|
|
369
|
-
}
|
|
363
|
+
});
|
|
370
364
|
|
|
371
365
|
expect(resolvedIds).not.toContain(requestId);
|
|
372
366
|
expect(pendingStore.has(requestId)).toBe(true);
|
|
@@ -388,19 +382,17 @@ describe("handleHostBashResult", () => {
|
|
|
388
382
|
).toThrow(ForbiddenError);
|
|
389
383
|
});
|
|
390
384
|
|
|
391
|
-
test("ForbiddenError message names both the submitting and expected client", () => {
|
|
385
|
+
test("ForbiddenError message names both the submitting and expected client", async () => {
|
|
392
386
|
const requestId = "req-targeted-mismatch-msg";
|
|
393
387
|
registerPending(requestId, { targetClientId: "client-abc" });
|
|
394
388
|
|
|
395
389
|
let caught: unknown;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
});
|
|
401
|
-
} catch (e) {
|
|
390
|
+
await handleHostBashResult({
|
|
391
|
+
body: bashBody(requestId),
|
|
392
|
+
headers: { "x-vellum-client-id": "client-xyz" },
|
|
393
|
+
}).catch((e: unknown) => {
|
|
402
394
|
caught = e;
|
|
403
|
-
}
|
|
395
|
+
});
|
|
404
396
|
|
|
405
397
|
expect(caught).toBeInstanceOf(ForbiddenError);
|
|
406
398
|
const msg = (caught as ForbiddenError).message;
|
|
@@ -408,18 +400,16 @@ describe("handleHostBashResult", () => {
|
|
|
408
400
|
expect(msg).toContain("client-abc");
|
|
409
401
|
});
|
|
410
402
|
|
|
411
|
-
test("interaction is NOT resolved on 403 (still pending)", () => {
|
|
403
|
+
test("interaction is NOT resolved on 403 (still pending)", async () => {
|
|
412
404
|
const requestId = "req-targeted-mismatch-stays";
|
|
413
405
|
registerPending(requestId, { targetClientId: "client-abc" });
|
|
414
406
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
});
|
|
420
|
-
} catch {
|
|
407
|
+
await handleHostBashResult({
|
|
408
|
+
body: bashBody(requestId),
|
|
409
|
+
headers: { "x-vellum-client-id": "client-xyz" },
|
|
410
|
+
}).catch(() => {
|
|
421
411
|
// expected
|
|
422
|
-
}
|
|
412
|
+
});
|
|
423
413
|
|
|
424
414
|
expect(resolvedIds).not.toContain(requestId);
|
|
425
415
|
expect(pendingStore.has(requestId)).toBe(true);
|
|
@@ -39,7 +39,7 @@ afterAll(() => {
|
|
|
39
39
|
|
|
40
40
|
const handleHostBrowserResult = ROUTES.find(
|
|
41
41
|
(r) => r.endpoint === "host-browser-result",
|
|
42
|
-
)!.handler
|
|
42
|
+
)!.handler as (args: Record<string, unknown>) => Promise<unknown>;
|
|
43
43
|
|
|
44
44
|
// ── Tests ────────────────────────────────────────────────────────────
|
|
45
45
|
|
|
@@ -212,7 +212,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
|
|
|
212
212
|
).toThrow(BadRequestError);
|
|
213
213
|
});
|
|
214
214
|
|
|
215
|
-
test("interaction is NOT consumed on 400 (still pending)", () => {
|
|
215
|
+
test("interaction is NOT consumed on 400 (still pending)", async () => {
|
|
216
216
|
const requestId = "browser-req-targeted-no-header-stays";
|
|
217
217
|
pendingInteractions.register(requestId, {
|
|
218
218
|
conversationId: "conv-1",
|
|
@@ -221,13 +221,11 @@ describe("handleHostBrowserResult — same-actor guard", () => {
|
|
|
221
221
|
targetActorPrincipalId: "user-1",
|
|
222
222
|
});
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
});
|
|
228
|
-
} catch {
|
|
224
|
+
await handleHostBrowserResult({
|
|
225
|
+
body: { requestId, content: "ok", isError: false },
|
|
226
|
+
}).catch(() => {
|
|
229
227
|
// expected
|
|
230
|
-
}
|
|
228
|
+
});
|
|
231
229
|
|
|
232
230
|
expect(pendingInteractions.get(requestId)).toBeDefined();
|
|
233
231
|
});
|
|
@@ -256,7 +254,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
|
|
|
256
254
|
).toThrow(ForbiddenError);
|
|
257
255
|
});
|
|
258
256
|
|
|
259
|
-
test("ForbiddenError message names both submitting and expected client", () => {
|
|
257
|
+
test("ForbiddenError message names both submitting and expected client", async () => {
|
|
260
258
|
const requestId = "browser-req-targeted-mismatch-msg";
|
|
261
259
|
pendingInteractions.register(requestId, {
|
|
262
260
|
conversationId: "conv-1",
|
|
@@ -267,7 +265,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
|
|
|
267
265
|
|
|
268
266
|
let caught: unknown;
|
|
269
267
|
try {
|
|
270
|
-
handleHostBrowserResult({
|
|
268
|
+
await handleHostBrowserResult({
|
|
271
269
|
body: { requestId, content: "ok", isError: false },
|
|
272
270
|
headers: {
|
|
273
271
|
"x-vellum-client-id": "client-B",
|
|
@@ -284,7 +282,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
|
|
|
284
282
|
expect(msg).toContain("client-B");
|
|
285
283
|
});
|
|
286
284
|
|
|
287
|
-
test("interaction is NOT consumed on 403 (still pending)", () => {
|
|
285
|
+
test("interaction is NOT consumed on 403 (still pending)", async () => {
|
|
288
286
|
const requestId = "browser-req-targeted-mismatch-stays";
|
|
289
287
|
pendingInteractions.register(requestId, {
|
|
290
288
|
conversationId: "conv-1",
|
|
@@ -293,17 +291,15 @@ describe("handleHostBrowserResult — same-actor guard", () => {
|
|
|
293
291
|
targetActorPrincipalId: "user-1",
|
|
294
292
|
});
|
|
295
293
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
});
|
|
304
|
-
} catch {
|
|
294
|
+
await handleHostBrowserResult({
|
|
295
|
+
body: { requestId, content: "ok", isError: false },
|
|
296
|
+
headers: {
|
|
297
|
+
"x-vellum-client-id": "client-B",
|
|
298
|
+
"x-vellum-actor-principal-id": "user-1",
|
|
299
|
+
},
|
|
300
|
+
}).catch(() => {
|
|
305
301
|
// expected
|
|
306
|
-
}
|
|
302
|
+
});
|
|
307
303
|
|
|
308
304
|
expect(pendingInteractions.get(requestId)).toBeDefined();
|
|
309
305
|
});
|
|
@@ -369,7 +365,7 @@ describe("handleHostBrowserResult — same-actor guard", () => {
|
|
|
369
365
|
).toThrow(ForbiddenError);
|
|
370
366
|
});
|
|
371
367
|
|
|
372
|
-
test("interaction NOT consumed on actor-mismatch 403", () => {
|
|
368
|
+
test("interaction NOT consumed on actor-mismatch 403", async () => {
|
|
373
369
|
const requestId = "browser-req-actor-mismatch-stays";
|
|
374
370
|
pendingInteractions.register(requestId, {
|
|
375
371
|
conversationId: "conv-1",
|
|
@@ -378,17 +374,15 @@ describe("handleHostBrowserResult — same-actor guard", () => {
|
|
|
378
374
|
targetActorPrincipalId: "user-1",
|
|
379
375
|
});
|
|
380
376
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
});
|
|
389
|
-
} catch {
|
|
377
|
+
await handleHostBrowserResult({
|
|
378
|
+
body: { requestId, content: "ok", isError: false },
|
|
379
|
+
headers: {
|
|
380
|
+
"x-vellum-client-id": "client-A",
|
|
381
|
+
"x-vellum-actor-principal-id": "user-2",
|
|
382
|
+
},
|
|
383
|
+
}).catch(() => {
|
|
390
384
|
// expected
|
|
391
|
-
}
|
|
385
|
+
});
|
|
392
386
|
|
|
393
387
|
expect(pendingInteractions.get(requestId)).toBeDefined();
|
|
394
388
|
});
|
|
@@ -477,6 +477,305 @@ describe("HostCuProxy", () => {
|
|
|
477
477
|
expect(result2.content).not.toContain("NO VISIBLE EFFECT");
|
|
478
478
|
});
|
|
479
479
|
|
|
480
|
+
test("skips unchanged warning and counter after a selection-only key (cmd+a)", async () => {
|
|
481
|
+
setup();
|
|
482
|
+
|
|
483
|
+
// Establish previous AX tree
|
|
484
|
+
const p1 = proxy.request(
|
|
485
|
+
"computer_use_click",
|
|
486
|
+
{ element_id: 1 },
|
|
487
|
+
"session-1",
|
|
488
|
+
1,
|
|
489
|
+
);
|
|
490
|
+
proxy.recordAction("computer_use_click", { element_id: 1 });
|
|
491
|
+
const sent1 = sentMessages[0] as Record<string, unknown>;
|
|
492
|
+
proxy.processObservation(sent1.requestId as string, {
|
|
493
|
+
axTree: "TextField [1]",
|
|
494
|
+
});
|
|
495
|
+
await p1;
|
|
496
|
+
|
|
497
|
+
// cmd+a only changes selection — AX tree can't show it, so empty diff
|
|
498
|
+
// is expected and must NOT warn or bump the unchanged counter.
|
|
499
|
+
const p2 = proxy.request(
|
|
500
|
+
"computer_use_key",
|
|
501
|
+
{ key: "cmd+a" },
|
|
502
|
+
"session-1",
|
|
503
|
+
2,
|
|
504
|
+
);
|
|
505
|
+
proxy.recordAction("computer_use_key", { key: "cmd+a" });
|
|
506
|
+
const sent2 = sentMessages[1] as Record<string, unknown>;
|
|
507
|
+
proxy.processObservation(sent2.requestId as string, {
|
|
508
|
+
axTree: "TextField [1]",
|
|
509
|
+
// No axDiff — selection change is invisible in the AX tree
|
|
510
|
+
});
|
|
511
|
+
const result2 = await p2;
|
|
512
|
+
expect(result2.content).not.toContain("NO VISIBLE EFFECT");
|
|
513
|
+
expect(proxy.consecutiveUnchangedSteps).toBe(0);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("exempts key combos regardless of spacing, case, alias, or modifier order", async () => {
|
|
517
|
+
// All of these normalize to an exempt combo the mac helper would execute
|
|
518
|
+
// identically: spaced, uppercase, alt->option alias, command->cmd alias,
|
|
519
|
+
// and reversed modifier order.
|
|
520
|
+
const variants = ["cmd + a", "CMD+A", "alt+tab", "command+c", "tab+shift"];
|
|
521
|
+
|
|
522
|
+
for (const [i, variant] of variants.entries()) {
|
|
523
|
+
setup();
|
|
524
|
+
|
|
525
|
+
// Establish previous AX tree
|
|
526
|
+
const p1 = proxy.request(
|
|
527
|
+
"computer_use_click",
|
|
528
|
+
{ element_id: 1 },
|
|
529
|
+
`session-${i}`,
|
|
530
|
+
1,
|
|
531
|
+
);
|
|
532
|
+
proxy.recordAction("computer_use_click", { element_id: 1 });
|
|
533
|
+
const sent1 = sentMessages[0] as Record<string, unknown>;
|
|
534
|
+
proxy.processObservation(sent1.requestId as string, {
|
|
535
|
+
axTree: "TextField [1]",
|
|
536
|
+
});
|
|
537
|
+
await p1;
|
|
538
|
+
|
|
539
|
+
const p2 = proxy.request(
|
|
540
|
+
"computer_use_key",
|
|
541
|
+
{ key: variant },
|
|
542
|
+
`session-${i}`,
|
|
543
|
+
2,
|
|
544
|
+
);
|
|
545
|
+
proxy.recordAction("computer_use_key", { key: variant });
|
|
546
|
+
const sent2 = sentMessages[1] as Record<string, unknown>;
|
|
547
|
+
proxy.processObservation(sent2.requestId as string, {
|
|
548
|
+
axTree: "TextField [1]",
|
|
549
|
+
// No axDiff — invisible-by-design change
|
|
550
|
+
});
|
|
551
|
+
const result2 = await p2;
|
|
552
|
+
expect(result2.content).not.toContain("NO VISIBLE EFFECT");
|
|
553
|
+
expect(proxy.consecutiveUnchangedSteps).toBe(0);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("exempt key clears the no-effect streak between non-exempt no-ops", async () => {
|
|
558
|
+
setup();
|
|
559
|
+
|
|
560
|
+
// Establish previous AX tree
|
|
561
|
+
const p1 = proxy.request(
|
|
562
|
+
"computer_use_click",
|
|
563
|
+
{ element_id: 1 },
|
|
564
|
+
"session-1",
|
|
565
|
+
1,
|
|
566
|
+
);
|
|
567
|
+
proxy.recordAction("computer_use_click", { element_id: 1 });
|
|
568
|
+
const sent1 = sentMessages[0] as Record<string, unknown>;
|
|
569
|
+
proxy.processObservation(sent1.requestId as string, {
|
|
570
|
+
axTree: "TextField [1]",
|
|
571
|
+
});
|
|
572
|
+
await p1;
|
|
573
|
+
|
|
574
|
+
// Non-exempt no-diff click — streak = 1
|
|
575
|
+
const p2 = proxy.request(
|
|
576
|
+
"computer_use_click",
|
|
577
|
+
{ element_id: 1 },
|
|
578
|
+
"session-1",
|
|
579
|
+
2,
|
|
580
|
+
);
|
|
581
|
+
proxy.recordAction("computer_use_click", { element_id: 1 });
|
|
582
|
+
const sent2 = sentMessages[1] as Record<string, unknown>;
|
|
583
|
+
proxy.processObservation(sent2.requestId as string, {
|
|
584
|
+
axTree: "TextField [1]",
|
|
585
|
+
});
|
|
586
|
+
await p2;
|
|
587
|
+
expect(proxy.consecutiveUnchangedSteps).toBe(1);
|
|
588
|
+
|
|
589
|
+
// Exempt cmd+a no-diff — must CLEAR the streak, not just skip it
|
|
590
|
+
const p3 = proxy.request(
|
|
591
|
+
"computer_use_key",
|
|
592
|
+
{ key: "cmd+a" },
|
|
593
|
+
"session-1",
|
|
594
|
+
3,
|
|
595
|
+
);
|
|
596
|
+
proxy.recordAction("computer_use_key", { key: "cmd+a" });
|
|
597
|
+
const sent3 = sentMessages[2] as Record<string, unknown>;
|
|
598
|
+
proxy.processObservation(sent3.requestId as string, {
|
|
599
|
+
axTree: "TextField [1]",
|
|
600
|
+
});
|
|
601
|
+
await p3;
|
|
602
|
+
expect(proxy.consecutiveUnchangedSteps).toBe(0);
|
|
603
|
+
|
|
604
|
+
// Next non-exempt no-diff click — streak = 1 again, NOT a "2 consecutive"
|
|
605
|
+
// escalation bridged across the exempt keypress.
|
|
606
|
+
const p4 = proxy.request(
|
|
607
|
+
"computer_use_click",
|
|
608
|
+
{ element_id: 1 },
|
|
609
|
+
"session-1",
|
|
610
|
+
4,
|
|
611
|
+
);
|
|
612
|
+
proxy.recordAction("computer_use_click", { element_id: 1 });
|
|
613
|
+
const sent4 = sentMessages[3] as Record<string, unknown>;
|
|
614
|
+
proxy.processObservation(sent4.requestId as string, {
|
|
615
|
+
axTree: "TextField [1]",
|
|
616
|
+
});
|
|
617
|
+
const result4 = await p4;
|
|
618
|
+
expect(proxy.consecutiveUnchangedSteps).toBe(1);
|
|
619
|
+
expect(result4.content).not.toContain("2 consecutive");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("still warns for a non-exempt key (enter) with no diff", async () => {
|
|
623
|
+
setup();
|
|
624
|
+
|
|
625
|
+
// Establish previous AX tree
|
|
626
|
+
const p1 = proxy.request(
|
|
627
|
+
"computer_use_click",
|
|
628
|
+
{ element_id: 1 },
|
|
629
|
+
"session-1",
|
|
630
|
+
1,
|
|
631
|
+
);
|
|
632
|
+
proxy.recordAction("computer_use_click", { element_id: 1 });
|
|
633
|
+
const sent1 = sentMessages[0] as Record<string, unknown>;
|
|
634
|
+
proxy.processObservation(sent1.requestId as string, {
|
|
635
|
+
axTree: "Button [1]",
|
|
636
|
+
});
|
|
637
|
+
await p1;
|
|
638
|
+
|
|
639
|
+
// enter is not in the exempt set — an empty diff should still warn.
|
|
640
|
+
const p2 = proxy.request(
|
|
641
|
+
"computer_use_key",
|
|
642
|
+
{ key: "enter" },
|
|
643
|
+
"session-1",
|
|
644
|
+
2,
|
|
645
|
+
);
|
|
646
|
+
proxy.recordAction("computer_use_key", { key: "enter" });
|
|
647
|
+
const sent2 = sentMessages[1] as Record<string, unknown>;
|
|
648
|
+
proxy.processObservation(sent2.requestId as string, {
|
|
649
|
+
axTree: "Button [1]",
|
|
650
|
+
});
|
|
651
|
+
const result2 = await p2;
|
|
652
|
+
expect(result2.content).toContain("NO VISIBLE EFFECT");
|
|
653
|
+
expect(proxy.consecutiveUnchangedSteps).toBe(1);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("still warns for a non-exempt action (click) with no diff", async () => {
|
|
657
|
+
setup();
|
|
658
|
+
|
|
659
|
+
// Establish previous AX tree
|
|
660
|
+
const p1 = proxy.request(
|
|
661
|
+
"computer_use_click",
|
|
662
|
+
{ element_id: 1 },
|
|
663
|
+
"session-1",
|
|
664
|
+
1,
|
|
665
|
+
);
|
|
666
|
+
proxy.recordAction("computer_use_click", { element_id: 1 });
|
|
667
|
+
const sent1 = sentMessages[0] as Record<string, unknown>;
|
|
668
|
+
proxy.processObservation(sent1.requestId as string, {
|
|
669
|
+
axTree: "Button [1]",
|
|
670
|
+
});
|
|
671
|
+
await p1;
|
|
672
|
+
|
|
673
|
+
const p2 = proxy.request(
|
|
674
|
+
"computer_use_click",
|
|
675
|
+
{ element_id: 1 },
|
|
676
|
+
"session-1",
|
|
677
|
+
2,
|
|
678
|
+
);
|
|
679
|
+
proxy.recordAction("computer_use_click", { element_id: 1 });
|
|
680
|
+
const sent2 = sentMessages[1] as Record<string, unknown>;
|
|
681
|
+
proxy.processObservation(sent2.requestId as string, {
|
|
682
|
+
axTree: "Button [1]",
|
|
683
|
+
});
|
|
684
|
+
const result2 = await p2;
|
|
685
|
+
expect(result2.content).toContain("NO VISIBLE EFFECT");
|
|
686
|
+
expect(proxy.consecutiveUnchangedSteps).toBe(1);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("loop detection still fires for repeated identical exempt keys", () => {
|
|
690
|
+
setup();
|
|
691
|
+
|
|
692
|
+
// Even though `up` is exempt from the unchanged-effect warning, repeating
|
|
693
|
+
// the same key must still be caught by loop detection.
|
|
694
|
+
proxy.recordAction("computer_use_key", { key: "up" });
|
|
695
|
+
proxy.recordAction("computer_use_key", { key: "up" });
|
|
696
|
+
proxy.recordAction("computer_use_key", { key: "up" });
|
|
697
|
+
|
|
698
|
+
const result = proxy.formatObservation({
|
|
699
|
+
axTree: "Button [1]",
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
expect(result.content).toContain(
|
|
703
|
+
"WARNING: You've repeated the same action (computer_use_key) 3 times",
|
|
704
|
+
);
|
|
705
|
+
expect(result.content).not.toContain("NO VISIBLE EFFECT");
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("loop detection fires for the same exempt combo spelled differently", () => {
|
|
709
|
+
setup();
|
|
710
|
+
|
|
711
|
+
// Equivalent spellings the mac helper executes identically must count as
|
|
712
|
+
// the same action — otherwise a stuck session could repeat cmd+a forever
|
|
713
|
+
// (no-effect warning is suppressed for exempt keys, so loop detection is
|
|
714
|
+
// the only remaining guard).
|
|
715
|
+
proxy.recordAction("computer_use_key", { key: "cmd+a" });
|
|
716
|
+
proxy.recordAction("computer_use_key", { key: "command+a" });
|
|
717
|
+
proxy.recordAction("computer_use_key", { key: "cmd + a" });
|
|
718
|
+
|
|
719
|
+
const result = proxy.formatObservation({
|
|
720
|
+
axTree: "Button [1]",
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
expect(result.content).toContain(
|
|
724
|
+
"WARNING: You've repeated the same action (computer_use_key) 3 times",
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
test("loop detection ignores the same combo sent to different clients", () => {
|
|
729
|
+
setup();
|
|
730
|
+
|
|
731
|
+
// Same key, different target desktops — not a repeat on one UI, so loop
|
|
732
|
+
// detection must NOT fire. Routing fields are kept in the signature.
|
|
733
|
+
proxy.recordAction("computer_use_key", {
|
|
734
|
+
key: "cmd+a",
|
|
735
|
+
target_client_id: "client-a",
|
|
736
|
+
});
|
|
737
|
+
proxy.recordAction("computer_use_key", {
|
|
738
|
+
key: "cmd+a",
|
|
739
|
+
target_client_id: "client-b",
|
|
740
|
+
});
|
|
741
|
+
proxy.recordAction("computer_use_key", {
|
|
742
|
+
key: "cmd+a",
|
|
743
|
+
target_client_id: "client-c",
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
const result = proxy.formatObservation({
|
|
747
|
+
axTree: "Button [1]",
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
expect(result.content).not.toContain("WARNING: You've repeated");
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("loop detection fires for the same combo+client across spellings", () => {
|
|
754
|
+
setup();
|
|
755
|
+
|
|
756
|
+
// Same target client, equivalent spellings — still a repeat on one UI.
|
|
757
|
+
proxy.recordAction("computer_use_key", {
|
|
758
|
+
key: "cmd+a",
|
|
759
|
+
target_client_id: "client-a",
|
|
760
|
+
});
|
|
761
|
+
proxy.recordAction("computer_use_key", {
|
|
762
|
+
key: "command+a",
|
|
763
|
+
target_client_id: "client-a",
|
|
764
|
+
});
|
|
765
|
+
proxy.recordAction("computer_use_key", {
|
|
766
|
+
key: "cmd + a",
|
|
767
|
+
target_client_id: "client-a",
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
const result = proxy.formatObservation({
|
|
771
|
+
axTree: "Button [1]",
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
expect(result.content).toContain(
|
|
775
|
+
"WARNING: You've repeated the same action (computer_use_key) 3 times",
|
|
776
|
+
);
|
|
777
|
+
});
|
|
778
|
+
|
|
480
779
|
test("resets consecutive count when diff is present", async () => {
|
|
481
780
|
setup();
|
|
482
781
|
|
|
@@ -104,7 +104,7 @@ afterAll(() => {
|
|
|
104
104
|
|
|
105
105
|
const handleHostCuResult = ROUTES.find(
|
|
106
106
|
(r: { endpoint: string }) => r.endpoint === "host-cu-result",
|
|
107
|
-
)!.handler
|
|
107
|
+
)!.handler as (args: Record<string, unknown>) => Promise<unknown>;
|
|
108
108
|
|
|
109
109
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
110
110
|
|
|
@@ -226,15 +226,13 @@ describe("handleHostCuResult — Phase 2 targetClientId guard", () => {
|
|
|
226
226
|
).toThrow(BadRequestError);
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
-
test("interaction is NOT resolved on 400 (still pending)", () => {
|
|
229
|
+
test("interaction is NOT resolved on 400 (still pending)", async () => {
|
|
230
230
|
const requestId = "req-cu-targeted-no-header-stays";
|
|
231
231
|
registerPending(requestId, { targetClientId: "client-A" });
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
handleHostCuResult({ body: cuBody(requestId) });
|
|
235
|
-
} catch {
|
|
233
|
+
await handleHostCuResult({ body: cuBody(requestId) }).catch(() => {
|
|
236
234
|
// expected
|
|
237
|
-
}
|
|
235
|
+
});
|
|
238
236
|
|
|
239
237
|
expect(resolvedIds).not.toContain(requestId);
|
|
240
238
|
expect(pendingStore.has(requestId)).toBe(true);
|
|
@@ -256,19 +254,17 @@ describe("handleHostCuResult — Phase 2 targetClientId guard", () => {
|
|
|
256
254
|
).toThrow(ForbiddenError);
|
|
257
255
|
});
|
|
258
256
|
|
|
259
|
-
test("ForbiddenError message names both submitting and expected client", () => {
|
|
257
|
+
test("ForbiddenError message names both submitting and expected client", async () => {
|
|
260
258
|
const requestId = "req-cu-targeted-mismatch-msg";
|
|
261
259
|
registerPending(requestId, { targetClientId: "client-A" });
|
|
262
260
|
|
|
263
261
|
let caught: unknown;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
});
|
|
269
|
-
} catch (e) {
|
|
262
|
+
await handleHostCuResult({
|
|
263
|
+
body: cuBody(requestId),
|
|
264
|
+
headers: { "x-vellum-client-id": "client-B" },
|
|
265
|
+
}).catch((e: unknown) => {
|
|
270
266
|
caught = e;
|
|
271
|
-
}
|
|
267
|
+
});
|
|
272
268
|
|
|
273
269
|
expect(caught).toBeInstanceOf(ForbiddenError);
|
|
274
270
|
const msg = (caught as ForbiddenError).message;
|
|
@@ -276,18 +272,16 @@ describe("handleHostCuResult — Phase 2 targetClientId guard", () => {
|
|
|
276
272
|
expect(msg).toContain("client-A");
|
|
277
273
|
});
|
|
278
274
|
|
|
279
|
-
test("interaction is NOT consumed on 403 (pendingInteractions.get still returns it)", () => {
|
|
275
|
+
test("interaction is NOT consumed on 403 (pendingInteractions.get still returns it)", async () => {
|
|
280
276
|
const requestId = "req-cu-targeted-mismatch-stays";
|
|
281
277
|
registerPending(requestId, { targetClientId: "client-A" });
|
|
282
278
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
});
|
|
288
|
-
} catch {
|
|
279
|
+
await handleHostCuResult({
|
|
280
|
+
body: cuBody(requestId),
|
|
281
|
+
headers: { "x-vellum-client-id": "client-B" },
|
|
282
|
+
}).catch(() => {
|
|
289
283
|
// expected
|
|
290
|
-
}
|
|
284
|
+
});
|
|
291
285
|
|
|
292
286
|
expect(resolvedIds).not.toContain(requestId);
|
|
293
287
|
expect(pendingStore.has(requestId)).toBe(true);
|
|
@@ -331,22 +325,20 @@ describe("handleHostCuResult — Phase 2 targetClientId guard", () => {
|
|
|
331
325
|
).toThrow(ForbiddenError);
|
|
332
326
|
});
|
|
333
327
|
|
|
334
|
-
test("interaction NOT consumed on 403 actor mismatch", () => {
|
|
328
|
+
test("interaction NOT consumed on 403 actor mismatch", async () => {
|
|
335
329
|
const requestId = "req-cu-actor-mismatch-stays";
|
|
336
330
|
actorPrincipalByClient.set("client-A", "user-1");
|
|
337
331
|
registerPending(requestId, { targetClientId: "client-A" });
|
|
338
332
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
});
|
|
347
|
-
} catch {
|
|
333
|
+
await handleHostCuResult({
|
|
334
|
+
body: cuBody(requestId),
|
|
335
|
+
headers: {
|
|
336
|
+
"x-vellum-client-id": "client-A",
|
|
337
|
+
"x-vellum-actor-principal-id": "user-2",
|
|
338
|
+
},
|
|
339
|
+
}).catch(() => {
|
|
348
340
|
// expected
|
|
349
|
-
}
|
|
341
|
+
});
|
|
350
342
|
|
|
351
343
|
expect(resolvedIds).not.toContain(requestId);
|
|
352
344
|
expect(pendingStore.has(requestId)).toBe(true);
|