@vellumai/assistant 0.9.1-staging.1 → 0.10.0-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/activation-funnel-telemetry.md +24 -18
- package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +15 -0
- package/openapi.yaml +852 -15
- package/package.json +1 -1
- package/src/__tests__/access-request-card-view.test.ts +98 -0
- package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +59 -7
- package/src/__tests__/agent-loop-compaction-strip.test.ts +17 -16
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
- package/src/__tests__/app-compiler.test.ts +15 -1
- package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
- package/src/__tests__/call-pointer-messages.test.ts +28 -0
- package/src/__tests__/cancel-clears-processing.test.ts +89 -0
- package/src/__tests__/channel-approval-routes.test.ts +0 -4
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
- package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
- package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
- package/src/__tests__/config-loader-backfill.test.ts +174 -30
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
- package/src/__tests__/contact-store-user-file.test.ts +0 -6
- package/src/__tests__/contacts-tools.test.ts +29 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +58 -0
- package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
- package/src/__tests__/conversation-lifecycle.test.ts +7 -9
- package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
- package/src/__tests__/conversation-title-service.test.ts +62 -0
- package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
- package/src/__tests__/credential-prompt-route.test.ts +1 -0
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/disk-pressure-policy.test.ts +12 -0
- package/src/__tests__/disk-usage.test.ts +65 -0
- package/src/__tests__/dynamic-page-surface.test.ts +51 -0
- package/src/__tests__/gateway-flag-listener.test.ts +110 -1
- package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
- package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-grant-minting.test.ts +3 -35
- package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
- package/src/__tests__/guardian-routing-state.test.ts +0 -1
- package/src/__tests__/headless-browser-mode.test.ts +10 -0
- package/src/__tests__/headless-browser-navigate.test.ts +8 -3
- package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
- package/src/__tests__/host-browser-proxy.test.ts +87 -0
- package/src/__tests__/injector-v3-suppression.test.ts +27 -20
- package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
- package/src/__tests__/invite-redemption-service.test.ts +0 -3
- package/src/__tests__/llm-catalog-parity.test.ts +30 -1
- package/src/__tests__/llm-resolver.test.ts +21 -0
- package/src/__tests__/llm-schema.test.ts +1 -0
- package/src/__tests__/managed-profile-guard.test.ts +163 -4
- package/src/__tests__/mcp-health-check.test.ts +6 -7
- package/src/__tests__/media-stream-server-integration.test.ts +317 -13
- package/src/__tests__/path-policy.test.ts +34 -0
- package/src/__tests__/persona-resolver.test.ts +38 -0
- package/src/__tests__/plugin-api-provider.test.ts +24 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
- package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
- package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
- package/src/__tests__/reaction-persistence.test.ts +150 -29
- package/src/__tests__/relay-server.test.ts +285 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
- package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
- package/src/__tests__/skill-execute-input.test.ts +5 -0
- package/src/__tests__/skills.test.ts +51 -0
- package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
- package/src/__tests__/subagent-tools.test.ts +150 -0
- package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
- package/src/__tests__/title-generate-hook.test.ts +100 -3
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
- package/src/__tests__/tool-audit-listener.test.ts +7 -7
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
- package/src/__tests__/trusted-contact-verification.test.ts +2 -4
- package/src/__tests__/twilio-routes.test.ts +81 -1
- package/src/__tests__/voice-invite-redemption.test.ts +0 -1
- package/src/__tests__/weak-open-model.test.ts +30 -0
- package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
- package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
- package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
- package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
- package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
- package/src/agent/loop.ts +33 -33
- package/src/api/events/tool-result.ts +6 -0
- package/src/api/events/workflow-completed.ts +53 -0
- package/src/api/events/workflow-leaf-finished.ts +38 -0
- package/src/api/events/workflow-leaf-started.ts +35 -0
- package/src/api/events/workflow-progress.ts +32 -0
- package/src/api/events/workflow-started.ts +31 -0
- package/src/api/index.ts +40 -0
- package/src/api/responses/conversation-message.ts +26 -0
- package/src/api/responses/home.ts +26 -0
- package/src/api/responses/workflow-journal.ts +53 -0
- package/src/approvals/guardian-card-withdrawal.ts +145 -0
- package/src/approvals/guardian-decision-primitive.ts +26 -3
- package/src/approvals/guardian-request-resolvers.ts +181 -78
- package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
- package/src/calls/call-pointer-messages.ts +10 -4
- package/src/calls/channel-admission-reader.ts +104 -0
- package/src/calls/guardian-dispatch.ts +17 -45
- package/src/calls/media-stream-server.ts +84 -2
- package/src/calls/relay-server.ts +66 -0
- package/src/calls/relay-setup-router.ts +82 -1
- package/src/calls/twilio-routes.ts +17 -8
- package/src/cli/commands/clients.ts +3 -0
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
- package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
- package/src/cli/commands/memory/index.ts +30 -0
- package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
- package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
- package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
- package/src/cli/commands/oauth/status.test.ts +36 -0
- package/src/cli/commands/oauth/status.ts +23 -3
- package/src/cli/commands/plugins.ts +57 -5
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +134 -4
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +53 -11
- package/src/cli/lib/inspect-plugin.ts +12 -1
- package/src/cli/lib/merge-plugin-tree.ts +149 -49
- package/src/cli/lib/plugin-surfaces.ts +104 -0
- package/src/cli/lib/upgrade-plugin.ts +64 -36
- package/src/cli/program.ts +2 -4
- package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
- package/src/config/assistant-feature-flags.ts +22 -7
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
- package/src/config/bundled-skills/messaging/SKILL.md +6 -4
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
- package/src/config/bundled-skills/workflows/SKILL.md +14 -7
- package/src/config/call-site-defaults.ts +3 -0
- package/src/config/feature-flag-registry.json +49 -18
- package/src/config/llm-resolver.ts +3 -0
- package/src/config/memory-v3-gate.ts +11 -0
- package/src/config/schema.ts +8 -6
- package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/channels.ts +11 -0
- package/src/config/schemas/llm.ts +31 -0
- package/src/config/schemas/memory-lifecycle.ts +3 -7
- package/src/config/schemas/memory-v3.ts +6 -0
- package/src/config/schemas/services.ts +18 -0
- package/src/config/seed-inference-profiles.ts +94 -34
- package/src/config/skills.ts +21 -0
- package/src/config/sync-gated-profiles.ts +220 -0
- package/src/contacts/contact-store.ts +2 -10
- package/src/contacts/contacts-write.ts +1 -2
- package/src/contacts/types.ts +0 -1
- package/src/context/compactor.ts +86 -52
- package/src/context/strip-injections.ts +58 -10
- package/src/context/token-estimator.ts +1 -1
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
- package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
- package/src/daemon/conversation-agent-loop.ts +100 -19
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-lifecycle.ts +3 -5
- package/src/daemon/conversation-process.ts +13 -5
- package/src/daemon/conversation-runtime-assembly.ts +13 -15
- package/src/daemon/conversation-surfaces.ts +26 -0
- package/src/daemon/conversation-tool-setup.ts +16 -11
- package/src/daemon/conversation.ts +64 -14
- package/src/daemon/disk-pressure-policy.ts +5 -3
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
- package/src/daemon/handlers/config-a2a.ts +0 -2
- package/src/daemon/handlers/config-channels.ts +5 -10
- package/src/daemon/handlers/config-slack-channel.ts +20 -0
- package/src/daemon/handlers/conversations.ts +107 -0
- package/src/daemon/host-browser-proxy.ts +41 -0
- package/src/daemon/lifecycle.ts +55 -20
- package/src/daemon/message-provenance.ts +2 -0
- package/src/daemon/message-types/contacts.ts +0 -1
- package/src/daemon/message-types/web-activity.ts +7 -1
- package/src/daemon/message-types/workflows.ts +83 -1
- package/src/daemon/tool-setup-types.ts +4 -0
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +2 -2
- package/src/home/feed-source-enrichment.test.ts +151 -0
- package/src/home/feed-source-enrichment.ts +176 -0
- package/src/instrument.ts +18 -6
- package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
- package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
- package/src/ipc/assistant-server.ts +37 -4
- package/src/ipc/gateway-flag-listener.ts +18 -2
- package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
- package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
- package/src/memory/__tests__/memory-retrospective-job.test.ts +34 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
- package/src/memory/auth-fallback-events-store.ts +2 -2
- package/src/memory/auto-analysis-enqueue.ts +3 -5
- package/src/memory/canonical-guardian-store.ts +39 -1
- package/src/memory/conversation-crud.ts +9 -4
- package/src/memory/conversation-key-store.ts +17 -2
- package/src/memory/conversation-title-service.ts +64 -7
- package/src/memory/db-init.ts +10 -0
- package/src/memory/embedding-backend.ts +15 -1
- package/src/memory/jobs-worker.ts +2 -1
- package/src/memory/lifecycle-events-store.ts +2 -2
- package/src/memory/memory-retrospective-enqueue.ts +31 -6
- package/src/memory/memory-retrospective-job.ts +9 -0
- package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
- package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
- package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
- package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +10 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
- package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
- package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
- package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
- package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
- package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
- package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
- package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
- package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +30 -0
- package/src/memory/migrations/index.ts +5 -0
- package/src/memory/onboarding-events-store.ts +3 -3
- package/src/memory/schema/contacts.ts +0 -1
- package/src/memory/skill-loaded-events-store.test.ts +7 -15
- package/src/memory/skill-loaded-events-store.ts +2 -2
- package/src/memory/tool-executed-events-store.test.ts +7 -7
- package/src/memory/turn-trace-store.test.ts +736 -0
- package/src/memory/turn-trace-store.ts +364 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
- package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
- package/src/memory/v2/consolidation-job.ts +2 -2
- package/src/memory/v2/skill-content.ts +25 -7
- package/src/memory/v2/skill-store.ts +7 -1
- package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
- package/src/memory/v3-eval/eval-packets.ts +546 -0
- package/src/messaging/providers/slack/api.ts +31 -0
- package/src/messaging/providers/slack/send.test.ts +114 -2
- package/src/messaging/providers/slack/send.ts +30 -7
- package/src/messaging/providers/slack/withdraw.test.ts +200 -0
- package/src/messaging/providers/slack/withdraw.ts +161 -0
- package/src/notifications/AGENTS.md +2 -0
- package/src/notifications/access-request-copy.ts +72 -59
- package/src/notifications/adapters/slack.ts +55 -73
- package/src/notifications/approval-card-data.ts +333 -0
- package/src/notifications/broadcaster.ts +6 -2
- package/src/notifications/canonical-delivery-recorder.ts +139 -0
- package/src/notifications/copy-composer.ts +3 -3
- package/src/notifications/decision-engine.ts +4 -2
- package/src/notifications/destination-resolver.ts +4 -6
- package/src/notifications/guardian-question-mode.ts +10 -0
- package/src/notifications/home-feed-side-effect.ts +3 -13
- package/src/notifications/notification-utils.ts +2 -1
- package/src/notifications/signal.ts +79 -43
- package/src/notifications/types.ts +98 -128
- package/src/permissions/checker.test.ts +51 -0
- package/src/permissions/checker.ts +185 -26
- package/src/permissions/ipc-risk-types.ts +24 -0
- package/src/permissions/question-prompter.test.ts +27 -0
- package/src/permissions/question-prompter.ts +4 -0
- package/src/platform/client.test.ts +119 -0
- package/src/platform/client.ts +66 -0
- package/src/platform/consent-cache.test.ts +267 -0
- package/src/platform/consent-cache.ts +174 -0
- package/src/plugin-api/index.ts +27 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
- package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
- package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
- package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
- package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
- package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
- package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
- package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
- package/src/plugins/defaults/advisor/config.ts +21 -0
- package/src/plugins/defaults/advisor/consult.ts +93 -0
- package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
- package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
- package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
- package/src/plugins/defaults/advisor/package.json +14 -0
- package/src/plugins/defaults/advisor/steering.ts +67 -0
- package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
- package/src/plugins/defaults/advisor/transcript.ts +76 -0
- package/src/plugins/defaults/index.ts +35 -0
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
- package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
- package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +75 -7
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +37 -4
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
- package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -12
- package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
- package/src/prompts/persona-resolver.ts +12 -2
- package/src/prompts/templates/system-sections.ts +7 -2
- package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
- package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
- package/src/providers/__tests__/retry-callsite.test.ts +176 -0
- package/src/providers/atlascloud/client.ts +85 -0
- package/src/providers/fetch-provider-catalog.ts +85 -0
- package/src/providers/inference/adapter-factory.ts +3 -0
- package/src/providers/model-catalog.ts +58 -0
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
- package/src/providers/openai/chat-completions-provider.ts +7 -0
- package/src/providers/openai/responses-provider.ts +10 -0
- package/src/providers/provider-send-message.ts +11 -3
- package/src/providers/retry.ts +53 -12
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/weak-open-model.ts +22 -0
- package/src/runtime/__tests__/agent-wake.test.ts +181 -0
- package/src/runtime/__tests__/client-health.test.ts +44 -0
- package/src/runtime/access-request-helper.ts +21 -53
- package/src/runtime/actor-trust-resolver.ts +49 -21
- package/src/runtime/agent-wake.ts +52 -0
- package/src/runtime/assistant-event-hub.ts +18 -4
- package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
- package/src/runtime/auth/require-bound-guardian.ts +1 -4
- package/src/runtime/capabilities.test.ts +120 -0
- package/src/runtime/capabilities.ts +197 -0
- package/src/runtime/channel-approval-types.ts +5 -1
- package/src/runtime/channel-retry-sweep.ts +1 -0
- package/src/runtime/channel-verification-service.ts +1 -2
- package/src/runtime/client-health.ts +26 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
- package/src/runtime/effective-capabilities.test.ts +128 -0
- package/src/runtime/effective-capabilities.ts +84 -0
- package/src/runtime/guardian-reply-router.ts +106 -21
- package/src/runtime/invite-redemption-service.ts +6 -22
- package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
- package/src/runtime/migrations/vbundle-builder.ts +49 -20
- package/src/runtime/pending-interactions.ts +15 -0
- package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +35 -13
- package/src/runtime/routes/browser-tabs-routes.ts +9 -0
- package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
- package/src/runtime/routes/client-routes.ts +10 -0
- package/src/runtime/routes/contact-routes.ts +31 -8
- package/src/runtime/routes/conversation-management-routes.ts +80 -1
- package/src/runtime/routes/conversation-query-routes.ts +68 -22
- package/src/runtime/routes/conversation-routes.ts +37 -12
- package/src/runtime/routes/events-routes.ts +1 -3
- package/src/runtime/routes/guardian-approval-interception.ts +14 -73
- package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
- package/src/runtime/routes/home-feed-routes.ts +8 -3
- package/src/runtime/routes/inbound-message-handler.ts +214 -228
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +88 -6
- package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
- package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
- package/src/runtime/routes/integrations/slack/channel.ts +36 -0
- package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
- package/src/runtime/routes/mcp-auth-routes.ts +233 -41
- package/src/runtime/routes/memory-eval-routes.ts +87 -0
- package/src/runtime/routes/notification-routes.ts +122 -133
- package/src/runtime/routes/platform-routes.ts +2 -2
- package/src/runtime/routes/plugins-routes.ts +40 -7
- package/src/runtime/routes/secret-routes.ts +10 -0
- package/src/runtime/routes/surface-action-routes.ts +2 -1
- package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
- package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
- package/src/runtime/routes/workflow-routes.test.ts +225 -1
- package/src/runtime/routes/workflow-routes.ts +131 -1
- package/src/runtime/tool-grant-request-helper.ts +18 -16
- package/src/runtime/trust-context-resolver.ts +8 -5
- package/src/schedule/schedule-store.ts +1 -1
- package/src/schedule/scheduler-types.ts +5 -1
- package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
- package/src/security/secret-patterns.ts +3 -0
- package/src/subagent/manager.ts +11 -4
- package/src/telemetry/trace-collection-policy.test.ts +28 -0
- package/src/telemetry/trace-collection-policy.ts +30 -0
- package/src/telemetry/types.ts +89 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
- package/src/telemetry/usage-telemetry-reporter.ts +148 -41
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
- package/src/tools/browser/browser-execution.ts +29 -18
- package/src/tools/document/document-tool.ts +2 -3
- package/src/tools/executor.ts +5 -3
- package/src/tools/host-terminal/host-shell.ts +5 -4
- package/src/tools/memory/register.ts +2 -2
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
- package/src/tools/network/__tests__/web-search.test.ts +143 -0
- package/src/tools/network/web-fetch.ts +372 -1
- package/src/tools/network/web-search.ts +213 -10
- package/src/tools/permission-checker.ts +3 -2
- package/src/tools/registry.ts +20 -0
- package/src/tools/schedule/create.ts +4 -3
- package/src/tools/schedule/update.ts +2 -1
- package/src/tools/shared/filesystem/path-policy.ts +39 -13
- package/src/tools/skills/execute.ts +1 -2
- package/src/tools/subagent/spawn.ts +37 -13
- package/src/tools/terminal/shell.ts +10 -4
- package/src/tools/tool-approval-handler.ts +17 -10
- package/src/tools/types.ts +9 -0
- package/src/tools/ui-surface/definitions.ts +25 -2
- package/src/tools/verification-control-plane-policy.ts +3 -1
- package/src/tools/workflows/run-workflow.ts +1 -0
- package/src/util/disk-usage.ts +78 -23
- package/src/util/platform.ts +8 -1
- package/src/watcher/telemetry.ts +2 -2
- package/src/workflows/engine.test.ts +175 -1
- package/src/workflows/engine.ts +82 -0
- package/src/workflows/journal-store.test.ts +70 -0
- package/src/workflows/journal-store.ts +18 -3
- package/src/workflows/run-manager.test.ts +171 -3
- package/src/workflows/run-manager.ts +64 -0
- package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
- package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
- package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
- package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/notifications/tool-approval-copy.ts +0 -142
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-compaction re-injection idempotency.
|
|
3
|
+
*
|
|
4
|
+
* The agent loop hands the post-compaction hook the full injected continuation
|
|
5
|
+
* history (it does not pre-strip it), so the hook must clear the tail's stale
|
|
6
|
+
* per-turn injection blocks before re-applying them. Without that strip,
|
|
7
|
+
* `applyRuntimeInjections` double-stacks every non-presence-gated block —
|
|
8
|
+
* `<turn_context>`, `<config_reset_notice>`, `<active_documents>`,
|
|
9
|
+
* `<document_comments>` — because it appends to the tail without removing an
|
|
10
|
+
* existing copy. These tests cover the strip primitive directly and the
|
|
11
|
+
* end-to-end re-injection it protects.
|
|
12
|
+
*/
|
|
13
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
clearConversations,
|
|
17
|
+
setConversation,
|
|
18
|
+
} from "../daemon/conversation-registry.js";
|
|
19
|
+
import { applyRuntimeInjections } from "../daemon/conversation-runtime-assembly.js";
|
|
20
|
+
import { stripTailInjectionsForReinjection } from "../plugins/defaults/memory-retrieval/tail-reinjection-strip.js";
|
|
21
|
+
import type { Message } from "../providers/types.js";
|
|
22
|
+
|
|
23
|
+
const WORKSPACE_BLOCK = "<workspace>\nRoot: /sandbox\n</workspace>";
|
|
24
|
+
const INFO_BLOCK = "<info>\nRemembered fact about project-x\n</info>";
|
|
25
|
+
const TURN_CONTEXT_BLOCK =
|
|
26
|
+
"<turn_context>\ncurrent_time: noon\n</turn_context>";
|
|
27
|
+
const CONFIG_RESET_BLOCK =
|
|
28
|
+
"<config_reset_notice>\nSettings were reset.\n</config_reset_notice>";
|
|
29
|
+
const ACTIVE_DOCUMENTS_BLOCK =
|
|
30
|
+
"<active_documents>\nThe following documents are open: notes.md\n</active_documents>";
|
|
31
|
+
const DOCUMENT_COMMENTS_BLOCK =
|
|
32
|
+
"<document_comments>\nOpen comments on notes.md\n</document_comments>";
|
|
33
|
+
const TURN_BODY = "Please continue the task.";
|
|
34
|
+
|
|
35
|
+
function userMsg(...texts: string[]): Message {
|
|
36
|
+
return {
|
|
37
|
+
role: "user",
|
|
38
|
+
content: texts.map((text) => ({ type: "text" as const, text })),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assistantMsg(text: string): Message {
|
|
43
|
+
return { role: "assistant", content: [{ type: "text", text }] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function tailContentTexts(messages: Message[]): string[] {
|
|
47
|
+
const last = messages[messages.length - 1];
|
|
48
|
+
return last.content.map((block) => (block.type === "text" ? block.text : ""));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function countTailBlocksWithPrefix(
|
|
52
|
+
messages: Message[],
|
|
53
|
+
prefix: string,
|
|
54
|
+
): number {
|
|
55
|
+
return tailContentTexts(messages).filter((text) => text.startsWith(prefix))
|
|
56
|
+
.length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("stripTailInjectionsForReinjection", () => {
|
|
60
|
+
test("clears every per-turn injection block from the tail user message", () => {
|
|
61
|
+
// GIVEN a tail user message carrying the real turn body plus the full
|
|
62
|
+
// per-turn injection set, including the four blocks compaction keeps in
|
|
63
|
+
// durable history
|
|
64
|
+
const messages: Message[] = [
|
|
65
|
+
userMsg(
|
|
66
|
+
TURN_BODY,
|
|
67
|
+
WORKSPACE_BLOCK,
|
|
68
|
+
INFO_BLOCK,
|
|
69
|
+
TURN_CONTEXT_BLOCK,
|
|
70
|
+
CONFIG_RESET_BLOCK,
|
|
71
|
+
ACTIVE_DOCUMENTS_BLOCK,
|
|
72
|
+
DOCUMENT_COMMENTS_BLOCK,
|
|
73
|
+
),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// WHEN the tail injections are stripped for re-injection
|
|
77
|
+
const result = stripTailInjectionsForReinjection(messages);
|
|
78
|
+
|
|
79
|
+
// THEN only the real turn body survives on the tail
|
|
80
|
+
expect(tailContentTexts(result)).toEqual([TURN_BODY]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("leaves injection blocks on earlier messages untouched", () => {
|
|
84
|
+
// GIVEN an earlier user message and the tail both carrying a turn-context
|
|
85
|
+
// block
|
|
86
|
+
const messages: Message[] = [
|
|
87
|
+
userMsg("Earlier turn", TURN_CONTEXT_BLOCK),
|
|
88
|
+
assistantMsg("Working on it."),
|
|
89
|
+
userMsg(TURN_BODY, TURN_CONTEXT_BLOCK),
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// WHEN the tail injections are stripped
|
|
93
|
+
const result = stripTailInjectionsForReinjection(messages);
|
|
94
|
+
|
|
95
|
+
// THEN the earlier message keeps its historical turn-context grounding
|
|
96
|
+
expect(result[0].content).toHaveLength(2);
|
|
97
|
+
expect((result[0].content[1] as { text: string }).text).toBe(
|
|
98
|
+
TURN_CONTEXT_BLOCK,
|
|
99
|
+
);
|
|
100
|
+
// AND only the tail is cleared
|
|
101
|
+
expect(tailContentTexts(result)).toEqual([TURN_BODY]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("returns the messages unchanged when the tail carries no injections", () => {
|
|
105
|
+
// GIVEN a tail with only real user content
|
|
106
|
+
const messages: Message[] = [userMsg(TURN_BODY)];
|
|
107
|
+
|
|
108
|
+
// WHEN the tail injections are stripped
|
|
109
|
+
const result = stripTailInjectionsForReinjection(messages);
|
|
110
|
+
|
|
111
|
+
// THEN the array is returned unchanged
|
|
112
|
+
expect(result).toBe(messages);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("preserves the tail user message even when every block is stripped", () => {
|
|
116
|
+
// GIVEN a tail composed entirely of injection blocks
|
|
117
|
+
const messages: Message[] = [
|
|
118
|
+
assistantMsg("Earlier reply."),
|
|
119
|
+
userMsg(WORKSPACE_BLOCK, TURN_CONTEXT_BLOCK),
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// WHEN the tail injections are stripped
|
|
123
|
+
const result = stripTailInjectionsForReinjection(messages);
|
|
124
|
+
|
|
125
|
+
// THEN the tail user message remains so the re-injection tail invariant
|
|
126
|
+
// holds, just with empty content
|
|
127
|
+
expect(result).toHaveLength(2);
|
|
128
|
+
expect(result[1].role).toBe("user");
|
|
129
|
+
expect(result[1].content).toHaveLength(0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("is idempotent — stripping an already-stripped tail is a no-op", () => {
|
|
133
|
+
// GIVEN a tail that has already been stripped once
|
|
134
|
+
const messages: Message[] = [userMsg(TURN_BODY, TURN_CONTEXT_BLOCK)];
|
|
135
|
+
const once = stripTailInjectionsForReinjection(messages);
|
|
136
|
+
|
|
137
|
+
// WHEN it is stripped again
|
|
138
|
+
const twice = stripTailInjectionsForReinjection(once);
|
|
139
|
+
|
|
140
|
+
// THEN the second strip changes nothing
|
|
141
|
+
expect(twice).toBe(once);
|
|
142
|
+
expect(tailContentTexts(twice)).toEqual([TURN_BODY]);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("applyRuntimeInjections re-injection idempotency", () => {
|
|
147
|
+
// `applyRuntimeInjections` synthesizes this conversation id when no
|
|
148
|
+
// turnContext is supplied, so the injectors resolve their blocks from the
|
|
149
|
+
// registry under this key.
|
|
150
|
+
const FALLBACK_CONVERSATION_ID = "runtime-assembly-fallback";
|
|
151
|
+
|
|
152
|
+
// Seed the fallback conversation with a workspace block (presence-gated) and
|
|
153
|
+
// a frozen temporal snapshot so the non-presence-gated `<turn_context>` block
|
|
154
|
+
// is produced every assembly.
|
|
155
|
+
function seedConversation(): void {
|
|
156
|
+
setConversation(FALLBACK_CONVERSATION_ID, {
|
|
157
|
+
conversationId: FALLBACK_CONVERSATION_ID,
|
|
158
|
+
workingDir: "/sandbox",
|
|
159
|
+
workspaceTopLevelContext: WORKSPACE_BLOCK,
|
|
160
|
+
workspaceTopLevelDirty: false,
|
|
161
|
+
currentTurnTemporalSnapshot: { clientTimezone: null },
|
|
162
|
+
} as never);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
afterEach(() => {
|
|
166
|
+
clearConversations();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("re-injecting an already-injected base without the strip double-stacks the non-presence-gated block", async () => {
|
|
170
|
+
// GIVEN a fresh turn assembled once, so its tail carries one workspace and
|
|
171
|
+
// one turn-context block
|
|
172
|
+
seedConversation();
|
|
173
|
+
const { messages: injectedOnce } = await applyRuntimeInjections(
|
|
174
|
+
[userMsg(TURN_BODY)],
|
|
175
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
176
|
+
);
|
|
177
|
+
expect(countTailBlocksWithPrefix(injectedOnce, "<turn_context>")).toBe(1);
|
|
178
|
+
|
|
179
|
+
// WHEN injections are applied again to that already-injected base
|
|
180
|
+
const { messages: injectedTwice } = await applyRuntimeInjections(
|
|
181
|
+
injectedOnce,
|
|
182
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// THEN the non-presence-gated turn-context block double-stacks
|
|
186
|
+
expect(countTailBlocksWithPrefix(injectedTwice, "<turn_context>")).toBe(2);
|
|
187
|
+
// AND the presence-gated workspace block stays single
|
|
188
|
+
expect(countTailBlocksWithPrefix(injectedTwice, "<workspace>")).toBe(1);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("stripping the tail before re-injecting keeps every block single", async () => {
|
|
192
|
+
// GIVEN a fresh turn assembled once
|
|
193
|
+
seedConversation();
|
|
194
|
+
const { messages: injectedOnce } = await applyRuntimeInjections(
|
|
195
|
+
[userMsg(TURN_BODY)],
|
|
196
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// WHEN the tail is stripped before re-injection, exactly as the
|
|
200
|
+
// post-compaction hook does
|
|
201
|
+
const { messages: reinjected } = await applyRuntimeInjections(
|
|
202
|
+
stripTailInjectionsForReinjection(injectedOnce),
|
|
203
|
+
{ conversationId: FALLBACK_CONVERSATION_ID },
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// THEN both the non-presence-gated and presence-gated blocks stay single
|
|
207
|
+
expect(countTailBlocksWithPrefix(reinjected, "<turn_context>")).toBe(1);
|
|
208
|
+
expect(countTailBlocksWithPrefix(reinjected, "<workspace>")).toBe(1);
|
|
209
|
+
// AND the real turn body survives
|
|
210
|
+
expect(
|
|
211
|
+
tailContentTexts(reinjected).some((text) => text === TURN_BODY),
|
|
212
|
+
).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -261,3 +261,79 @@ describe("SendMessageOptions.config.overrideProfile", () => {
|
|
|
261
261
|
expect(captured?.overrideProfile).toBeUndefined();
|
|
262
262
|
});
|
|
263
263
|
});
|
|
264
|
+
|
|
265
|
+
describe("SendMessageOptions.config.forceOverrideProfile", () => {
|
|
266
|
+
test("CallSiteConfiguredProvider forwards forceOverrideProfile into the send config", async () => {
|
|
267
|
+
let captured: SendMessageOptions | undefined;
|
|
268
|
+
const inner: Provider = {
|
|
269
|
+
name: "anthropic",
|
|
270
|
+
async sendMessage(
|
|
271
|
+
_messages: Message[],
|
|
272
|
+
options?: SendMessageOptions,
|
|
273
|
+
): Promise<ProviderResponse> {
|
|
274
|
+
captured = options;
|
|
275
|
+
return makeResponse("anthropic");
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const provider = new CallSiteConfiguredProvider(
|
|
280
|
+
inner,
|
|
281
|
+
"inference",
|
|
282
|
+
"strong",
|
|
283
|
+
true,
|
|
284
|
+
);
|
|
285
|
+
await provider.sendMessage(DUMMY_MESSAGES, {});
|
|
286
|
+
|
|
287
|
+
expect(captured?.config).toMatchObject({
|
|
288
|
+
callSite: "inference",
|
|
289
|
+
overrideProfile: "strong",
|
|
290
|
+
forceOverrideProfile: true,
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("forceOverrideProfile floats the override above a call-site profile pin", async () => {
|
|
295
|
+
// The advisor scenario in miniature: the `inference` call site is pinned to
|
|
296
|
+
// a cheap profile, but a caller forces a stronger profile for its own send.
|
|
297
|
+
setLlmConfig({
|
|
298
|
+
default: { provider: "anthropic", model: "claude-opus-4-7" },
|
|
299
|
+
profiles: {
|
|
300
|
+
cheap: { provider: "anthropic", model: "claude-haiku-4-5-20251001" },
|
|
301
|
+
strong: { provider: "anthropic", model: "claude-opus-4-8" },
|
|
302
|
+
},
|
|
303
|
+
callSites: { inference: { profile: "cheap" } },
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const send = async (force: boolean) => {
|
|
307
|
+
let captured: Record<string, unknown> | undefined;
|
|
308
|
+
const inner: Provider = {
|
|
309
|
+
name: "anthropic",
|
|
310
|
+
async sendMessage(
|
|
311
|
+
_messages: Message[],
|
|
312
|
+
options?: SendMessageOptions,
|
|
313
|
+
): Promise<ProviderResponse> {
|
|
314
|
+
captured = options?.config as Record<string, unknown> | undefined;
|
|
315
|
+
return makeResponse("anthropic");
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
const provider = new RetryProvider(inner);
|
|
319
|
+
await provider.sendMessage(DUMMY_MESSAGES, {
|
|
320
|
+
config: {
|
|
321
|
+
callSite: "inference",
|
|
322
|
+
overrideProfile: "strong",
|
|
323
|
+
...(force ? { forceOverrideProfile: true } : {}),
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
return captured;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Without the flag, the call-site pin (`cheap`) outranks `overrideProfile`.
|
|
330
|
+
expect((await send(false))?.model).toBe("claude-haiku-4-5-20251001");
|
|
331
|
+
|
|
332
|
+
// With the flag, the forced `strong` profile wins over the call-site pin.
|
|
333
|
+
const forced = await send(true);
|
|
334
|
+
expect(forced?.model).toBe("claude-opus-4-8");
|
|
335
|
+
// The routing keys are stripped before the provider wire request.
|
|
336
|
+
expect(forced?.overrideProfile).toBeUndefined();
|
|
337
|
+
expect(forced?.forceOverrideProfile).toBeUndefined();
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -46,23 +46,20 @@ import { eq } from "drizzle-orm";
|
|
|
46
46
|
|
|
47
47
|
import { upsertContactChannel } from "../contacts/contacts-write.js";
|
|
48
48
|
import type { Conversation } from "../daemon/conversation.js";
|
|
49
|
+
import {
|
|
50
|
+
createCanonicalGuardianDelivery,
|
|
51
|
+
createCanonicalGuardianRequest,
|
|
52
|
+
getCanonicalGuardianRequest,
|
|
53
|
+
} from "../memory/canonical-guardian-store.js";
|
|
49
54
|
import { getDb } from "../memory/db-connection.js";
|
|
50
55
|
import { initializeDb } from "../memory/db-init.js";
|
|
51
|
-
import {
|
|
52
|
-
createApprovalRequest,
|
|
53
|
-
getPendingApprovalForRequest,
|
|
54
|
-
} from "../memory/guardian-approvals.js";
|
|
55
56
|
import { messages } from "../memory/schema/conversations.js";
|
|
56
57
|
import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
|
|
57
58
|
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
58
|
-
import {
|
|
59
|
-
_clearApprovalPromptTsTrackerForTesting,
|
|
60
|
-
trackApprovalPromptTs,
|
|
61
|
-
} from "../runtime/routes/approval-prompt-ts-tracker.js";
|
|
62
59
|
import {
|
|
63
60
|
isSlackReactionEvent,
|
|
64
61
|
parseSlackReactionCallbackData,
|
|
65
|
-
} from "../runtime/routes/inbound-
|
|
62
|
+
} from "../runtime/routes/inbound-stages/reaction-intercept.js";
|
|
66
63
|
import { handleChannelInbound } from "./helpers/channel-test-adapter.js";
|
|
67
64
|
import { createGuardianBinding } from "./helpers/create-guardian-binding.js";
|
|
68
65
|
|
|
@@ -84,7 +81,6 @@ function resetState(): void {
|
|
|
84
81
|
db.run("DELETE FROM conversations");
|
|
85
82
|
db.run("DELETE FROM contact_channels");
|
|
86
83
|
db.run("DELETE FROM contacts");
|
|
87
|
-
_clearApprovalPromptTsTrackerForTesting();
|
|
88
84
|
}
|
|
89
85
|
|
|
90
86
|
function seedActiveMember(): void {
|
|
@@ -421,21 +417,35 @@ function seedGuardianForChannel(): void {
|
|
|
421
417
|
function seedPendingGuardianApprovalForReaction(
|
|
422
418
|
requestId: string,
|
|
423
419
|
conversationId: string,
|
|
420
|
+
reactedTs: string,
|
|
424
421
|
): void {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
422
|
+
// Canonical tool_approval request keyed by the confirmation requestId — the
|
|
423
|
+
// same record assistant-event-hub creates for every confirmation.
|
|
424
|
+
createCanonicalGuardianRequest({
|
|
425
|
+
id: requestId,
|
|
426
|
+
kind: "tool_approval",
|
|
427
|
+
sourceType: "channel",
|
|
428
|
+
sourceChannel: "slack",
|
|
428
429
|
conversationId,
|
|
429
|
-
channel: "slack",
|
|
430
430
|
requesterExternalUserId: "requester-user-1",
|
|
431
431
|
requesterChatId: SLACK_CHANNEL_ID,
|
|
432
432
|
guardianExternalUserId: GUARDIAN_USER_ID,
|
|
433
|
-
|
|
433
|
+
guardianPrincipalId: GUARDIAN_USER_ID,
|
|
434
434
|
toolName: GUARDIAN_REACTION_TOOL,
|
|
435
|
+
status: "pending",
|
|
435
436
|
expiresAt: Date.now() + 300_000,
|
|
436
437
|
});
|
|
437
438
|
|
|
438
|
-
//
|
|
439
|
+
// The delivered approval card → request mapping the reaction resolves
|
|
440
|
+
// against (destination_message_id = the reacted Slack ts).
|
|
441
|
+
createCanonicalGuardianDelivery({
|
|
442
|
+
requestId,
|
|
443
|
+
destinationChannel: "slack",
|
|
444
|
+
destinationChatId: SLACK_CHANNEL_ID,
|
|
445
|
+
destinationMessageId: reactedTs,
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Register a pending interaction so the tool_approval resolver finds the
|
|
439
449
|
// requester-side hook to drive `allow`.
|
|
440
450
|
const handleConfirmationResponse = mock(() => {});
|
|
441
451
|
const _mockSession = {
|
|
@@ -466,18 +476,17 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
|
|
|
466
476
|
db.run("DELETE FROM conversations");
|
|
467
477
|
db.run("DELETE FROM contact_channels");
|
|
468
478
|
db.run("DELETE FROM contacts");
|
|
469
|
-
db.run("DELETE FROM
|
|
479
|
+
db.run("DELETE FROM canonical_guardian_requests");
|
|
480
|
+
db.run("DELETE FROM canonical_guardian_deliveries");
|
|
470
481
|
pendingInteractions.clear();
|
|
471
|
-
_clearApprovalPromptTsTrackerForTesting();
|
|
472
482
|
msgCounter = 0;
|
|
473
483
|
});
|
|
474
484
|
|
|
475
485
|
test("guardian reaction on pending approval applies decision and persists no transcript row", async () => {
|
|
476
486
|
seedGuardianForChannel();
|
|
477
487
|
const requestId = "req-guardian-react-1";
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
// ACL allows. Easier: insert directly via the DB layer.
|
|
488
|
+
// Back the canonical request and its pending interaction with a real
|
|
489
|
+
// conversation row, inserted directly via the DB layer.
|
|
481
490
|
const db = getDb();
|
|
482
491
|
const conversationId = "conv-react-test-1";
|
|
483
492
|
const now = Date.now();
|
|
@@ -491,12 +500,15 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
|
|
|
491
500
|
)
|
|
492
501
|
.run(conversationId, now, now);
|
|
493
502
|
|
|
494
|
-
seedPendingGuardianApprovalForReaction(requestId, conversationId);
|
|
495
|
-
|
|
496
503
|
const reactedTs = "1700000099.000001";
|
|
497
|
-
//
|
|
498
|
-
// guardian reaction is scoped to a
|
|
499
|
-
|
|
504
|
+
// The approval card was delivered on this ts; its canonical delivery row
|
|
505
|
+
// is how the guardian reaction is scoped to a known approval message.
|
|
506
|
+
seedPendingGuardianApprovalForReaction(
|
|
507
|
+
requestId,
|
|
508
|
+
conversationId,
|
|
509
|
+
reactedTs,
|
|
510
|
+
);
|
|
511
|
+
|
|
500
512
|
const body = {
|
|
501
513
|
sourceChannel: "slack",
|
|
502
514
|
interface: "slack",
|
|
@@ -541,11 +553,11 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
|
|
|
541
553
|
|
|
542
554
|
expect(resp.status).toBe(200);
|
|
543
555
|
expect(json.accepted).toBe(true);
|
|
544
|
-
expect(json.
|
|
556
|
+
expect(json.canonicalRouter).toBe("canonical_decision_applied");
|
|
545
557
|
expect(approvalConversationGenerator).not.toHaveBeenCalled();
|
|
546
558
|
|
|
547
|
-
// The
|
|
548
|
-
expect(
|
|
559
|
+
// The canonical request is resolved (no longer pending).
|
|
560
|
+
expect(getCanonicalGuardianRequest(requestId)?.status).toBe("approved");
|
|
549
561
|
|
|
550
562
|
// No transcript row was written for the reaction itself — resolved
|
|
551
563
|
// guardian approval reactions have no transcript representation.
|
|
@@ -563,3 +575,112 @@ describe("guardian approval-by-reaction integration via handleChannelInbound", (
|
|
|
563
575
|
expect(reactionRows.length).toBe(0);
|
|
564
576
|
});
|
|
565
577
|
});
|
|
578
|
+
|
|
579
|
+
// ---------------------------------------------------------------------------
|
|
580
|
+
// Reaction access-control regression (LUM-2489)
|
|
581
|
+
// ---------------------------------------------------------------------------
|
|
582
|
+
//
|
|
583
|
+
// A reaction is a passive signal, not an access attempt. Because reactions are
|
|
584
|
+
// dispatched before the ingress pipeline, an unknown user's 👍 must never run
|
|
585
|
+
// ACL (no trusted-contact verification handshake), create a conversation, or
|
|
586
|
+
// write a binding — it is dropped as channel noise. A known contact's reaction
|
|
587
|
+
// is recorded; neither triggers a verification challenge.
|
|
588
|
+
|
|
589
|
+
describe("reaction access control (no verification handshake)", () => {
|
|
590
|
+
const STRANGER_USER_ID = "U_REACTION_STRANGER";
|
|
591
|
+
const CONTACT_USER_ID = "U_REACTION_CONTACT";
|
|
592
|
+
// Guardian's approval channel is a DM, distinct from the public channel the
|
|
593
|
+
// reaction lands in (mirroring production). Reusing the public channel id
|
|
594
|
+
// would let a reactor match the guardian's channel via findContactChannel's
|
|
595
|
+
// externalChatId fallback and mask the bug.
|
|
596
|
+
const GUARDIAN_DM_CHAT = "D_GUARDIAN_DM";
|
|
597
|
+
|
|
598
|
+
function tableCount(table: string): number {
|
|
599
|
+
return (
|
|
600
|
+
getDb().$client.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get() as {
|
|
601
|
+
n: number;
|
|
602
|
+
}
|
|
603
|
+
).n;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
beforeEach(() => {
|
|
607
|
+
resetState();
|
|
608
|
+
getDb().run("DELETE FROM channel_verification_sessions");
|
|
609
|
+
// The assistant has a guardian (as in production); the reactors below are
|
|
610
|
+
// different users.
|
|
611
|
+
createGuardianBinding({
|
|
612
|
+
channel: "slack",
|
|
613
|
+
guardianExternalUserId: GUARDIAN_USER_ID,
|
|
614
|
+
guardianDeliveryChatId: GUARDIAN_DM_CHAT,
|
|
615
|
+
guardianPrincipalId: GUARDIAN_USER_ID,
|
|
616
|
+
});
|
|
617
|
+
msgCounter = 0;
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("stranger's reaction is dropped — no challenge, session, conversation, or row", async () => {
|
|
621
|
+
let agentDispatched = false;
|
|
622
|
+
const processMessage = async (): Promise<{ messageId: string }> => {
|
|
623
|
+
agentDispatched = true;
|
|
624
|
+
return { messageId: "should-not-be-called" };
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
const req = buildReactionRequest("reaction:thumbsup", {
|
|
628
|
+
actorExternalId: STRANGER_USER_ID,
|
|
629
|
+
actorDisplayName: "Outside Reactor",
|
|
630
|
+
actorUsername: "outsider",
|
|
631
|
+
});
|
|
632
|
+
const resp = await handleChannelInbound(
|
|
633
|
+
req,
|
|
634
|
+
processMessage,
|
|
635
|
+
TEST_BEARER_TOKEN,
|
|
636
|
+
);
|
|
637
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
638
|
+
|
|
639
|
+
// Accepted as a passive signal — never denied or turned into a challenge.
|
|
640
|
+
expect(json.accepted).toBe(true);
|
|
641
|
+
expect(json.denied).not.toBe(true);
|
|
642
|
+
expect(json.reason).not.toBe("verification_challenge_sent");
|
|
643
|
+
expect(json.verificationSessionId).toBeUndefined();
|
|
644
|
+
expect(agentDispatched).toBe(false);
|
|
645
|
+
|
|
646
|
+
// No verification handshake, and no side effects: a dropped reaction leaves
|
|
647
|
+
// no transcript row, no conversation, and no binding.
|
|
648
|
+
expect(tableCount("channel_verification_sessions")).toBe(0);
|
|
649
|
+
expect(readPersistedMessages().length).toBe(0);
|
|
650
|
+
expect(tableCount("conversations")).toBe(0);
|
|
651
|
+
expect(tableCount("external_conversation_bindings")).toBe(0);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("known contact's reaction is recorded — no challenge", async () => {
|
|
655
|
+
// A pending contact classifies as `unverified_contact` — a known tier, so
|
|
656
|
+
// its reactions are recorded. On a real message it would be re-challenged,
|
|
657
|
+
// but a reaction must not trigger that.
|
|
658
|
+
upsertContactChannel({
|
|
659
|
+
sourceChannel: "slack",
|
|
660
|
+
externalUserId: CONTACT_USER_ID,
|
|
661
|
+
externalChatId: SLACK_CHANNEL_ID,
|
|
662
|
+
status: "pending",
|
|
663
|
+
policy: "allow",
|
|
664
|
+
displayName: "Pending Contact",
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const req = buildReactionRequest("reaction:tada", {
|
|
668
|
+
actorExternalId: CONTACT_USER_ID,
|
|
669
|
+
actorDisplayName: "Pending Contact",
|
|
670
|
+
actorUsername: "pending_contact",
|
|
671
|
+
});
|
|
672
|
+
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
673
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
674
|
+
|
|
675
|
+
expect(json.denied).not.toBe(true);
|
|
676
|
+
expect(json.reason).not.toBe("verification_challenge_sent");
|
|
677
|
+
expect(tableCount("channel_verification_sessions")).toBe(0);
|
|
678
|
+
|
|
679
|
+
const rows = readPersistedMessages();
|
|
680
|
+
expect(rows.length).toBe(1);
|
|
681
|
+
const envelope = JSON.parse(rows[0].metadata!) as Record<string, unknown>;
|
|
682
|
+
const slackMeta = readSlackMetadata(envelope.slackMeta as string);
|
|
683
|
+
expect(slackMeta?.eventKind).toBe("reaction");
|
|
684
|
+
expect(slackMeta?.reaction?.emoji).toBe("tada");
|
|
685
|
+
});
|
|
686
|
+
});
|