@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
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
import * as Sentry from "@sentry/node";
|
|
35
35
|
|
|
36
|
+
import { recordWatchdogEvent } from "../memory/watchdog-events-store.js";
|
|
36
37
|
import { getLogger } from "../util/logger.js";
|
|
37
38
|
|
|
38
39
|
const log = getLogger("event-loop-watchdog");
|
|
@@ -74,11 +75,37 @@ export function evaluateTick(
|
|
|
74
75
|
return { blockedMs, exceeded: blockedMs >= thresholdMs };
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Check name emitted for event-loop block events. The platform's
|
|
80
|
+
* `watchdog__event_loop_blocking_daily` admin query filters `check_name` to
|
|
81
|
+
* this exact string, so it is the primary group-by dimension downstream —
|
|
82
|
+
* keep it stable.
|
|
83
|
+
*/
|
|
84
|
+
export const EVENT_LOOP_BLOCKED_CHECK_NAME = "event_loop_blocked";
|
|
85
|
+
|
|
77
86
|
function reportBlock(blockedMs: number, thresholdMs: number): void {
|
|
78
87
|
log.warn(
|
|
79
88
|
{ blockedMs, thresholdMs, tickIntervalMs: TICK_INTERVAL_MS },
|
|
80
89
|
"event loop blocked",
|
|
81
90
|
);
|
|
91
|
+
// Persist a `watchdog` telemetry event so the platform can surface
|
|
92
|
+
// event-loop blocking in the infrastructure admin chart. `recordWatchdogEvent`
|
|
93
|
+
// no-ops when usage-data collection is disabled (the event is dropped to
|
|
94
|
+
// honor the opt-out), so the watchdog runs unconditionally without leaking
|
|
95
|
+
// health data for opted-out owners. Never let a telemetry failure escape
|
|
96
|
+
// the timer callback — wrap it alongside the Sentry capture below.
|
|
97
|
+
try {
|
|
98
|
+
recordWatchdogEvent({
|
|
99
|
+
checkName: EVENT_LOOP_BLOCKED_CHECK_NAME,
|
|
100
|
+
value: blockedMs,
|
|
101
|
+
detail: {
|
|
102
|
+
threshold_ms: thresholdMs,
|
|
103
|
+
tick_interval_ms: TICK_INTERVAL_MS,
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
} catch {
|
|
107
|
+
// Never let a telemetry failure escape the timer callback.
|
|
108
|
+
}
|
|
82
109
|
try {
|
|
83
110
|
Sentry.withScope((scope) => {
|
|
84
111
|
scope.setLevel("warning");
|
|
@@ -88,7 +115,7 @@ function reportBlock(blockedMs: number, thresholdMs: number): void {
|
|
|
88
115
|
threshold_ms: thresholdMs,
|
|
89
116
|
tick_interval_ms: TICK_INTERVAL_MS,
|
|
90
117
|
});
|
|
91
|
-
Sentry.captureMessage(
|
|
118
|
+
Sentry.captureMessage(EVENT_LOOP_BLOCKED_CHECK_NAME);
|
|
92
119
|
});
|
|
93
120
|
} catch {
|
|
94
121
|
// Never let a telemetry failure escape the timer callback.
|
|
@@ -19,24 +19,20 @@
|
|
|
19
19
|
* dropped from the registry via {@link unregisterPlugin} so none of its
|
|
20
20
|
* hooks participate in the turn lifecycle. This is the primary mechanism for
|
|
21
21
|
* shipping experimental plugins behind a feature flag.
|
|
22
|
-
* 4.
|
|
23
|
-
* credential store helper ({@link getSecureKeyAsync}). In Docker mode
|
|
24
|
-
* that helper goes through the CES HTTP API transparently; in local mode
|
|
25
|
-
* it hits the encrypted file store / CES RPC backend.
|
|
26
|
-
* 5. Validates the config block under `plugins.<name>` against
|
|
22
|
+
* 4. Validates the config block under `plugins.<name>` against
|
|
27
23
|
* `manifest.config` if the manifest supplies a parser-like validator
|
|
28
24
|
* (Zod schemas with `.parse()` are supported; anything else is passed
|
|
29
25
|
* through untouched).
|
|
30
|
-
*
|
|
26
|
+
* 5. Creates `<workspaceDir>/plugins-data/<plugin>/` on demand for per-plugin
|
|
31
27
|
* writable state and exposes it via {@link PluginInitContext.pluginStorageDir}.
|
|
32
|
-
*
|
|
28
|
+
* 6. For each surviving plugin, registers its contributed tools and routes
|
|
33
29
|
* into their global registries via {@link registerPluginTools} and
|
|
34
30
|
* {@link registerSkillRoute}. Contributions land BEFORE `init()` so
|
|
35
31
|
* the plugin's hook can observe a registry where its own model-visible
|
|
36
32
|
* surface is already wired — useful for plugins that want to attach
|
|
37
33
|
* metadata, warm caches, or otherwise interact with their own
|
|
38
34
|
* contributions during initialization.
|
|
39
|
-
*
|
|
35
|
+
* 7. Awaits `plugin.init(ctx)` sequentially. An init failure is contained to
|
|
40
36
|
* the offending plugin: its already-registered tools and routes are rolled
|
|
41
37
|
* back, it is dropped from the registry, the failure is logged, and
|
|
42
38
|
* bootstrap continues with the remaining plugins. A single plugin's failure
|
|
@@ -76,7 +72,6 @@ import {
|
|
|
76
72
|
type SkillRouteHandle,
|
|
77
73
|
unregisterSkillRoute,
|
|
78
74
|
} from "../runtime/skill-route-registry.js";
|
|
79
|
-
import { getSecureKeyAsync } from "../security/secure-keys.js";
|
|
80
75
|
import {
|
|
81
76
|
registerPluginTools,
|
|
82
77
|
unregisterPluginTools,
|
|
@@ -88,25 +83,6 @@ import { registerShutdownHook } from "./shutdown-registry.js";
|
|
|
88
83
|
|
|
89
84
|
const log = getLogger("plugins-bootstrap");
|
|
90
85
|
|
|
91
|
-
/**
|
|
92
|
-
* Resolve one credential value. Returns the raw secret string or throws a
|
|
93
|
-
* {@link PluginExecutionError} tagged with the plugin name so the caller can
|
|
94
|
-
* fail startup with clear attribution.
|
|
95
|
-
*/
|
|
96
|
-
async function resolveCredentialOrThrow(
|
|
97
|
-
pluginName: string,
|
|
98
|
-
credentialKey: string,
|
|
99
|
-
): Promise<string> {
|
|
100
|
-
const value = await getSecureKeyAsync(credentialKey);
|
|
101
|
-
if (value === undefined || value === "") {
|
|
102
|
-
throw new PluginExecutionError(
|
|
103
|
-
`plugin ${pluginName} requires credential "${credentialKey}" but the credential store returned no value`,
|
|
104
|
-
pluginName,
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
return value;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
86
|
/**
|
|
111
87
|
* Validate a plugin config block. If the manifest supplies a parser-like
|
|
112
88
|
* validator (Zod schemas expose `.parse()`), use it. Otherwise pass the
|
|
@@ -346,11 +322,6 @@ async function initializePlugin(
|
|
|
346
322
|
let initCompleted = false;
|
|
347
323
|
|
|
348
324
|
try {
|
|
349
|
-
const credentials: Record<string, string> = {};
|
|
350
|
-
for (const key of plugin.manifest.requiresCredential ?? []) {
|
|
351
|
-
credentials[key] = await resolveCredentialOrThrow(name, key);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
325
|
const config = validatePluginConfig(
|
|
355
326
|
name,
|
|
356
327
|
plugin.manifest.config,
|
|
@@ -359,7 +330,6 @@ async function initializePlugin(
|
|
|
359
330
|
|
|
360
331
|
const initContext = {
|
|
361
332
|
config,
|
|
362
|
-
credentials,
|
|
363
333
|
logger: log.child({ plugin: name }),
|
|
364
334
|
pluginStorageDir: ensurePluginStorageDir(name),
|
|
365
335
|
assistantVersion: APP_VERSION,
|
|
@@ -129,4 +129,29 @@ describe("redeemA2AInvite", () => {
|
|
|
129
129
|
const result = redeemA2AInvite({ sender: SENDER });
|
|
130
130
|
expect(result.success).toBe(true);
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
test("activeConnections counts a2a channel existence, not status", () => {
|
|
134
|
+
const result = redeemA2AInvite({ sender: SENDER });
|
|
135
|
+
expect(result.success).toBe(true);
|
|
136
|
+
|
|
137
|
+
expect(getA2AConfig().activeConnections).toBe(1);
|
|
138
|
+
|
|
139
|
+
// Readiness is existence-based: a non-active stored status still counts.
|
|
140
|
+
const sqlite = getSqlite();
|
|
141
|
+
sqlite.run("UPDATE contact_channels SET status = 'unverified'");
|
|
142
|
+
invalidateConfigCache();
|
|
143
|
+
expect(getA2AConfig().activeConnections).toBe(1);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("already-connected guard fires regardless of stored channel status", () => {
|
|
147
|
+
const first = redeemA2AInvite({ sender: SENDER });
|
|
148
|
+
expect(first.success).toBe(true);
|
|
149
|
+
|
|
150
|
+
const sqlite = getSqlite();
|
|
151
|
+
sqlite.run("UPDATE contact_channels SET status = 'revoked'");
|
|
152
|
+
|
|
153
|
+
const second = redeemA2AInvite({ sender: SENDER });
|
|
154
|
+
expect(second.alreadyConnected).toBe(true);
|
|
155
|
+
expect(second.contactId).toBe(first.contactId);
|
|
156
|
+
});
|
|
132
157
|
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { GuardianDelivery } from "@vellumai/gateway-client";
|
|
4
|
+
|
|
5
|
+
import type { ContactChannel } from "../../../contacts/types.js";
|
|
6
|
+
|
|
7
|
+
let mockGuardians: GuardianDelivery[] | null = null;
|
|
8
|
+
let mockBinding: { guardianExternalUserId: string; guardianDeliveryChatId: string } | null = null;
|
|
9
|
+
let mockContactChannel: { channel: ContactChannel } | null = null;
|
|
10
|
+
let mockChannel: ContactChannel | null = null;
|
|
11
|
+
let mockGwContactChannels: Array<{ id: string; status: string; verifiedAt: number | null }> | null =
|
|
12
|
+
null;
|
|
13
|
+
let ipcCalls: Array<{ method: string; payload: unknown }> = [];
|
|
14
|
+
|
|
15
|
+
mock.module("../../../contacts/guardian-delivery-reader.js", () => ({
|
|
16
|
+
getGuardianDelivery: async (input?: { channelTypes?: string[] }) => {
|
|
17
|
+
if (mockGuardians == null) return null;
|
|
18
|
+
if (!input?.channelTypes) return mockGuardians;
|
|
19
|
+
return mockGuardians.filter((g) =>
|
|
20
|
+
input.channelTypes!.includes(g.channelType),
|
|
21
|
+
);
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
mock.module("../../../contacts/contact-store.js", () => ({
|
|
26
|
+
findContactChannel: () => mockContactChannel,
|
|
27
|
+
findGuardianForChannel: () => null,
|
|
28
|
+
getChannelById: () => mockChannel,
|
|
29
|
+
getContact: () => ({ id: "contact-1", displayName: "Pat" }),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
mock.module("../../../contacts/contact-events.js", () => ({
|
|
33
|
+
emitContactChange: () => {},
|
|
34
|
+
onContactChange: () => {},
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
mock.module("../../../ipc/gateway-client.js", () => ({
|
|
38
|
+
ipcCallPersistent: async (method: string, payload: unknown) => {
|
|
39
|
+
ipcCalls.push({ method, payload });
|
|
40
|
+
if (method === "contacts_get_rich") {
|
|
41
|
+
if (mockGwContactChannels == null) return { ok: true, contact: null };
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
contact: {
|
|
45
|
+
id: "contact-1",
|
|
46
|
+
displayName: "Pat",
|
|
47
|
+
role: "contact",
|
|
48
|
+
interactionCount: 0,
|
|
49
|
+
createdAt: 0,
|
|
50
|
+
updatedAt: 0,
|
|
51
|
+
channels: mockGwContactChannels.map((c) => ({
|
|
52
|
+
id: c.id,
|
|
53
|
+
contactId: "contact-1",
|
|
54
|
+
type: "telegram",
|
|
55
|
+
address: "user-123",
|
|
56
|
+
isPrimary: true,
|
|
57
|
+
externalUserId: null,
|
|
58
|
+
status: c.status,
|
|
59
|
+
policy: "allow",
|
|
60
|
+
verifiedAt: c.verifiedAt,
|
|
61
|
+
verifiedVia: null,
|
|
62
|
+
lastSeenAt: null,
|
|
63
|
+
interactionCount: 0,
|
|
64
|
+
lastInteraction: null,
|
|
65
|
+
revokedReason: null,
|
|
66
|
+
blockedReason: null,
|
|
67
|
+
})),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ok: true,
|
|
73
|
+
didWrite: true,
|
|
74
|
+
channel: {
|
|
75
|
+
id: "ch-1",
|
|
76
|
+
contactId: "contact-1",
|
|
77
|
+
type: "telegram",
|
|
78
|
+
address: "user-123",
|
|
79
|
+
status: "revoked",
|
|
80
|
+
revokedReason: "guardian_binding_revoked",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
ipcCall: async () => null,
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
mock.module("../../../runtime/channel-verification-service.js", () => ({
|
|
88
|
+
getGuardianBinding: () => mockBinding,
|
|
89
|
+
revokeBinding: () => true,
|
|
90
|
+
revokePendingSessions: () => {},
|
|
91
|
+
createOutboundSession: () => ({
|
|
92
|
+
sessionId: "sess",
|
|
93
|
+
secret: "code",
|
|
94
|
+
expiresAt: Date.now() + 1000,
|
|
95
|
+
}),
|
|
96
|
+
countRecentSendsToDestination: () => 0,
|
|
97
|
+
isGuardianBoundForChannel: async () => false,
|
|
98
|
+
updateSessionDelivery: () => {},
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
mock.module("../../../runtime/verification-outbound-actions.js", () => ({
|
|
102
|
+
cancelOutbound: () => {},
|
|
103
|
+
deliverVerificationEmail: () => {},
|
|
104
|
+
deliverVerificationSlack: () => {},
|
|
105
|
+
deliverVerificationTelegram: () => {},
|
|
106
|
+
DESTINATION_RATE_WINDOW_MS: 1000,
|
|
107
|
+
MAX_SENDS_PER_DESTINATION_WINDOW: 5,
|
|
108
|
+
normalizeTelegramDestination: (d: string) => d,
|
|
109
|
+
resendOutbound: () => ({}),
|
|
110
|
+
startOutbound: async () => ({}),
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
import {
|
|
114
|
+
revokeVerificationForChannel,
|
|
115
|
+
verifyTrustedContact,
|
|
116
|
+
} from "../config-channels.js";
|
|
117
|
+
|
|
118
|
+
function channel(overrides: Partial<ContactChannel> = {}): ContactChannel {
|
|
119
|
+
return {
|
|
120
|
+
id: "ch-1",
|
|
121
|
+
contactId: "contact-1",
|
|
122
|
+
type: "telegram",
|
|
123
|
+
address: "user-123",
|
|
124
|
+
isPrimary: true,
|
|
125
|
+
externalChatId: "chat-123",
|
|
126
|
+
// DB columns are intentionally a terminal state to prove the gates ignore
|
|
127
|
+
// them and read from the gateway delivery instead.
|
|
128
|
+
status: "revoked",
|
|
129
|
+
policy: {} as ContactChannel["policy"],
|
|
130
|
+
verifiedAt: null,
|
|
131
|
+
verifiedVia: null,
|
|
132
|
+
inviteId: null,
|
|
133
|
+
revokedReason: null,
|
|
134
|
+
blockedReason: null,
|
|
135
|
+
lastSeenAt: null,
|
|
136
|
+
interactionCount: 0,
|
|
137
|
+
lastInteraction: null,
|
|
138
|
+
updatedAt: null,
|
|
139
|
+
createdAt: 0,
|
|
140
|
+
...overrides,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function delivery(overrides: Partial<GuardianDelivery> = {}): GuardianDelivery {
|
|
145
|
+
return {
|
|
146
|
+
channelType: "telegram",
|
|
147
|
+
contactId: "contact-1",
|
|
148
|
+
address: "user-123",
|
|
149
|
+
externalChatId: "chat-123",
|
|
150
|
+
status: "active",
|
|
151
|
+
verifiedAt: 1700000000,
|
|
152
|
+
...overrides,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
describe("revokeVerificationForChannel", () => {
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
mockGuardians = null;
|
|
159
|
+
mockBinding = { guardianExternalUserId: "user-123", guardianDeliveryChatId: "chat-123" };
|
|
160
|
+
mockContactChannel = { channel: channel() };
|
|
161
|
+
ipcCalls = [];
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("relays mark_channel_revoked when the gateway delivery is live", async () => {
|
|
165
|
+
mockGuardians = [delivery({ status: "active" })];
|
|
166
|
+
await revokeVerificationForChannel("telegram");
|
|
167
|
+
expect(ipcCalls.map((c) => c.method)).toContain("mark_channel_revoked");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("skips a redundant revoke when the gateway delivery is already revoked", async () => {
|
|
171
|
+
// Local DB status is the live "active" here, but the gateway (SoT) says
|
|
172
|
+
// revoked — the gate must follow the gateway and not relay.
|
|
173
|
+
mockContactChannel = { channel: channel({ status: "active" }) };
|
|
174
|
+
mockGuardians = [delivery({ status: "revoked" })];
|
|
175
|
+
await revokeVerificationForChannel("telegram");
|
|
176
|
+
expect(ipcCalls.map((c) => c.method)).not.toContain("mark_channel_revoked");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("skips the relay when the gateway has no delivery for the channel", async () => {
|
|
180
|
+
mockGuardians = [];
|
|
181
|
+
await revokeVerificationForChannel("telegram");
|
|
182
|
+
expect(ipcCalls.map((c) => c.method)).not.toContain("mark_channel_revoked");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("skips the relay when the gateway is unreachable", async () => {
|
|
186
|
+
mockGuardians = null;
|
|
187
|
+
await revokeVerificationForChannel("telegram");
|
|
188
|
+
expect(ipcCalls.map((c) => c.method)).not.toContain("mark_channel_revoked");
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("verifyTrustedContact already-verified gate", () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
mockGuardians = null;
|
|
195
|
+
mockGwContactChannels = null;
|
|
196
|
+
mockChannel = channel();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("short-circuits when the gateway contact channel is active and verified", async () => {
|
|
200
|
+
// Arbitrary trusted contact (non-guardian) — read from the contact-channel
|
|
201
|
+
// gateway read, which covers all contacts.
|
|
202
|
+
mockGwContactChannels = [
|
|
203
|
+
{ id: "ch-1", status: "active", verifiedAt: 1700000000 },
|
|
204
|
+
];
|
|
205
|
+
const result = await verifyTrustedContact("ch-1", "assistant-1");
|
|
206
|
+
expect(result.success).toBe(false);
|
|
207
|
+
expect(result.error).toBe("already_verified");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("does not short-circuit when the gateway channel has no verifiedAt", async () => {
|
|
211
|
+
// DB column says verified, but the gateway channel is unverified — proceed.
|
|
212
|
+
mockChannel = channel({ status: "active", verifiedAt: 1700000000 });
|
|
213
|
+
mockGwContactChannels = [
|
|
214
|
+
{ id: "ch-1", status: "pending", verifiedAt: null },
|
|
215
|
+
];
|
|
216
|
+
const result = await verifyTrustedContact("ch-1", "assistant-1");
|
|
217
|
+
expect(result.error).not.toBe("already_verified");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("does not short-circuit when the gateway has no matching channel", async () => {
|
|
221
|
+
mockGwContactChannels = [];
|
|
222
|
+
const result = await verifyTrustedContact("ch-1", "assistant-1");
|
|
223
|
+
expect(result.error).not.toBe("already_verified");
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -96,13 +96,11 @@ export function getA2AConfig(): A2AConfigResult {
|
|
|
96
96
|
const config = getConfig();
|
|
97
97
|
const enabled = config.a2a?.enabled ?? false;
|
|
98
98
|
|
|
99
|
+
// a2a is peer binding outside the human-trust ACL model — the gateway has no
|
|
100
|
+
// canonical a2a channel status, so channel existence is the readiness signal.
|
|
99
101
|
const contacts = searchContacts({ channelType: "a2a" });
|
|
100
102
|
const activeConnections = contacts.reduce((count, c) => {
|
|
101
|
-
return (
|
|
102
|
-
count +
|
|
103
|
-
c.channels.filter((ch) => ch.type === "a2a" && ch.status === "active")
|
|
104
|
-
.length
|
|
105
|
-
);
|
|
103
|
+
return count + c.channels.filter((ch) => ch.type === "a2a").length;
|
|
106
104
|
}, 0);
|
|
107
105
|
|
|
108
106
|
return { success: true, enabled, activeConnections };
|
|
@@ -270,12 +268,9 @@ export function redeemA2AInvite(params: {
|
|
|
270
268
|
setA2AConfig();
|
|
271
269
|
}
|
|
272
270
|
|
|
273
|
-
// 2. Check for existing
|
|
271
|
+
// 2. Check for existing contact with this sender (a2a binding existence)
|
|
274
272
|
const existing = findContactByAddress("a2a", params.sender.assistantId);
|
|
275
|
-
if (
|
|
276
|
-
existing &&
|
|
277
|
-
existing.channels.some((ch) => ch.type === "a2a" && ch.status === "active")
|
|
278
|
-
) {
|
|
273
|
+
if (existing && existing.channels.some((ch) => ch.type === "a2a")) {
|
|
279
274
|
return { success: true, alreadyConnected: true, contactId: existing.id };
|
|
280
275
|
}
|
|
281
276
|
|
|
@@ -373,10 +368,7 @@ export async function acceptA2AInvite(params: {
|
|
|
373
368
|
// 2. Short-circuit if already connected — avoids a network round-trip
|
|
374
369
|
// and consuming a token on the sender side.
|
|
375
370
|
const existing = findContactByAddress("a2a", params.senderAssistantId);
|
|
376
|
-
if (
|
|
377
|
-
existing &&
|
|
378
|
-
existing.channels.some((ch) => ch.type === "a2a" && ch.status === "active")
|
|
379
|
-
) {
|
|
371
|
+
if (existing && existing.channels.some((ch) => ch.type === "a2a")) {
|
|
380
372
|
return { success: true, alreadyConnected: true, contactId: existing.id };
|
|
381
373
|
}
|
|
382
374
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { GuardianDelivery } from "@vellumai/gateway-client";
|
|
4
|
+
import {
|
|
5
|
+
GetContactIpcResponseSchema,
|
|
6
|
+
MarkChannelRevokedIpcResponseSchema,
|
|
7
|
+
} from "@vellumai/gateway-client/gateway-ipc-contracts";
|
|
4
8
|
|
|
5
9
|
import { startVerificationCall } from "../../calls/call-domain.js";
|
|
6
10
|
import type { ChannelId } from "../../channels/types.js";
|
|
@@ -11,7 +15,8 @@ import {
|
|
|
11
15
|
getChannelById,
|
|
12
16
|
getContact,
|
|
13
17
|
} from "../../contacts/contact-store.js";
|
|
14
|
-
import
|
|
18
|
+
import { getGuardianDelivery } from "../../contacts/guardian-delivery-reader.js";
|
|
19
|
+
import type { ContactChannel } from "../../contacts/types.js";
|
|
15
20
|
import { ipcCallPersistent } from "../../ipc/gateway-client.js";
|
|
16
21
|
import { getBindingByChannelChat } from "../../memory/external-conversation-store.js";
|
|
17
22
|
import { resolveGuardianName } from "../../prompts/user-reference.js";
|
|
@@ -28,6 +33,7 @@ import {
|
|
|
28
33
|
findActiveSession,
|
|
29
34
|
getGuardianBinding,
|
|
30
35
|
getPendingSession,
|
|
36
|
+
isGuardianBoundForChannel,
|
|
31
37
|
revokeBinding,
|
|
32
38
|
revokePendingSessions,
|
|
33
39
|
updateSessionDelivery,
|
|
@@ -76,23 +82,64 @@ export function getReadinessService(): ChannelReadinessService {
|
|
|
76
82
|
return _readinessService;
|
|
77
83
|
}
|
|
78
84
|
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Gateway delivery lookup
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve the gateway-owned delivery (ACL source of truth) for a contact
|
|
91
|
+
* channel, matching on type and either address or externalChatId. Returns
|
|
92
|
+
* `undefined` when the gateway is unreachable or has no binding for it.
|
|
93
|
+
*/
|
|
94
|
+
async function deliveryForChannel(
|
|
95
|
+
channel: Pick<ContactChannel, "type" | "address" | "externalChatId">,
|
|
96
|
+
): Promise<GuardianDelivery | undefined> {
|
|
97
|
+
const guardians = await getGuardianDelivery({ channelTypes: [channel.type] });
|
|
98
|
+
if (!guardians) return undefined;
|
|
99
|
+
return guardians.find(
|
|
100
|
+
(g) =>
|
|
101
|
+
g.channelType === channel.type &&
|
|
102
|
+
((channel.address && g.address === channel.address) ||
|
|
103
|
+
(channel.externalChatId != null &&
|
|
104
|
+
g.externalChatId === channel.externalChatId)),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Read a contact channel's verified state from the gateway contact-channel read
|
|
110
|
+
* (ACL source of truth). Covers all contacts, not just guardian deliveries.
|
|
111
|
+
* Returns `undefined` when the gateway is unreachable or has no such channel.
|
|
112
|
+
*/
|
|
113
|
+
async function gatewayContactChannelState(
|
|
114
|
+
channel: Pick<ContactChannel, "id" | "contactId">,
|
|
115
|
+
): Promise<{ status: string; verifiedAt: number | null } | undefined> {
|
|
116
|
+
const result = await ipcCallPersistent("contacts_get_rich", {
|
|
117
|
+
contactId: channel.contactId,
|
|
118
|
+
});
|
|
119
|
+
if (!result || (result as { contact?: unknown }).contact == null) {
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
const { contact } = GetContactIpcResponseSchema.parse(result);
|
|
123
|
+
const ch = contact.channels.find((c) => c.id === channel.id);
|
|
124
|
+
return ch ? { status: ch.status, verifiedAt: ch.verifiedAt } : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
79
127
|
// ---------------------------------------------------------------------------
|
|
80
128
|
// Extracted business logic functions
|
|
81
129
|
// ---------------------------------------------------------------------------
|
|
82
130
|
|
|
83
|
-
export function createInboundChallenge(
|
|
131
|
+
export async function createInboundChallenge(
|
|
84
132
|
channel?: ChannelId,
|
|
85
133
|
rebind?: boolean,
|
|
86
134
|
conversationId?: string,
|
|
87
|
-
): ChannelVerificationSessionResult {
|
|
88
|
-
const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
135
|
+
): Promise<ChannelVerificationSessionResult> {
|
|
89
136
|
const resolvedChannel = channel ?? "telegram";
|
|
90
137
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
);
|
|
95
|
-
if (
|
|
138
|
+
// Gateway-backed presence guard: block re-binding when a guardian is already
|
|
139
|
+
// bound. Null-list (gateway unreachable) is treated as bound, so a transient
|
|
140
|
+
// miss blocks rather than letting a second binding through.
|
|
141
|
+
const alreadyBound = await isGuardianBoundForChannel(resolvedChannel);
|
|
142
|
+
if (alreadyBound && !rebind) {
|
|
96
143
|
return {
|
|
97
144
|
success: false,
|
|
98
145
|
error: "already_bound",
|
|
@@ -115,13 +162,13 @@ export function createInboundChallenge(
|
|
|
115
162
|
};
|
|
116
163
|
}
|
|
117
164
|
|
|
118
|
-
export function getVerificationStatus(
|
|
165
|
+
export async function getVerificationStatus(
|
|
119
166
|
channel?: ChannelId,
|
|
120
|
-
): ChannelVerificationSessionResult {
|
|
167
|
+
): Promise<ChannelVerificationSessionResult> {
|
|
121
168
|
const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
122
169
|
const resolvedChannel = channel ?? "telegram";
|
|
123
170
|
|
|
124
|
-
const binding = getGuardianBinding(resolvedAssistantId, resolvedChannel);
|
|
171
|
+
const binding = await getGuardianBinding(resolvedAssistantId, resolvedChannel);
|
|
125
172
|
|
|
126
173
|
// Read the contact directly to get displayName — getGuardianBinding is a
|
|
127
174
|
// compatibility shim that doesn't carry metadataJson.
|
|
@@ -189,7 +236,10 @@ export async function revokeVerificationForChannel(
|
|
|
189
236
|
|
|
190
237
|
// Capture binding before revoking so we can downgrade the guardian's
|
|
191
238
|
// channel — without this, the guardian would still pass the ACL check.
|
|
192
|
-
const bindingBeforeRevoke = getGuardianBinding(
|
|
239
|
+
const bindingBeforeRevoke = await getGuardianBinding(
|
|
240
|
+
assistantId,
|
|
241
|
+
resolvedChannel,
|
|
242
|
+
);
|
|
193
243
|
if (!bindingBeforeRevoke) {
|
|
194
244
|
return {
|
|
195
245
|
success: true,
|
|
@@ -206,13 +256,16 @@ export async function revokeVerificationForChannel(
|
|
|
206
256
|
|
|
207
257
|
// Relay the ACL downgrade to the gateway (source of truth). The gateway's
|
|
208
258
|
// mark_channel_revoked enforces the guardian guard and dual-writes the
|
|
209
|
-
// contact-channel status back to the assistant DB.
|
|
259
|
+
// contact-channel status back to the assistant DB. Gate on the gateway
|
|
260
|
+
// delivery's live status, not the assistant DB column, so a redundant revoke
|
|
261
|
+
// is still skipped for an already-revoked binding.
|
|
210
262
|
if (contactResult) {
|
|
211
|
-
const
|
|
263
|
+
const delivery = await deliveryForChannel(contactResult.channel);
|
|
264
|
+
const deliveryStatus = delivery?.status;
|
|
212
265
|
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
266
|
+
deliveryStatus === "active" ||
|
|
267
|
+
deliveryStatus === "pending" ||
|
|
268
|
+
deliveryStatus === "unverified"
|
|
216
269
|
) {
|
|
217
270
|
const result = await ipcCallPersistent("mark_channel_revoked", {
|
|
218
271
|
contactChannelId: contactResult.channel.id,
|
|
@@ -294,7 +347,10 @@ export async function verifyTrustedContact(
|
|
|
294
347
|
};
|
|
295
348
|
}
|
|
296
349
|
|
|
297
|
-
|
|
350
|
+
// Already-verified short-circuit derived from the gateway contact-channel read
|
|
351
|
+
// (ACL SoT), which covers all contacts — not just guardian deliveries.
|
|
352
|
+
const gwState = await gatewayContactChannelState(channel);
|
|
353
|
+
if (gwState?.status === "active" && gwState.verifiedAt != null) {
|
|
298
354
|
return {
|
|
299
355
|
success: false,
|
|
300
356
|
error: "already_verified",
|
|
@@ -579,7 +635,7 @@ export async function handleChannelVerificationSession(
|
|
|
579
635
|
...publicResult,
|
|
580
636
|
});
|
|
581
637
|
} else {
|
|
582
|
-
const result = createInboundChallenge(
|
|
638
|
+
const result = await createInboundChallenge(
|
|
583
639
|
channel,
|
|
584
640
|
msg.rebind,
|
|
585
641
|
msg.conversationId,
|
|
@@ -590,7 +646,7 @@ export async function handleChannelVerificationSession(
|
|
|
590
646
|
});
|
|
591
647
|
}
|
|
592
648
|
} else if (msg.action === "status") {
|
|
593
|
-
const result = getVerificationStatus(channel);
|
|
649
|
+
const result = await getVerificationStatus(channel);
|
|
594
650
|
broadcastMessage({
|
|
595
651
|
type: "channel_verification_session_response",
|
|
596
652
|
...result,
|