@vellumai/assistant 0.10.4 → 0.10.5-dev.202607022329.d89bf2f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +94 -91
- package/Dockerfile +5 -16
- package/README.md +7 -10
- package/bun.lock +0 -4
- package/docs/architecture/security.md +20 -20
- package/docs/stt-provider-onboarding.md +4 -5
- package/docs/trusted-contact-access.md +7 -7
- package/eslint-rules/cli-no-daemon-internals.js +4 -0
- package/eslint.config.mjs +7 -1
- package/knip.json +0 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/contact-read-contracts.test.ts +36 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +233 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +40 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +20 -8
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +33 -0
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +216 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +91 -0
- package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +39 -3
- package/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
- package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
- package/openapi.yaml +1073 -225
- package/package.json +1 -4
- package/scripts/test.sh +35 -13
- package/src/__tests__/access-request-seed-content-blocks.test.ts +60 -4
- package/src/__tests__/acp-session.test.ts +4 -1
- package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +0 -6
- package/src/__tests__/add-message-preassigned-id.test.ts +56 -0
- package/src/__tests__/agent-loop-set-system-prompt.test.ts +139 -0
- package/src/__tests__/app-conversation-ids-backfill.test.ts +3 -0
- package/src/__tests__/app-source-watcher.test.ts +11 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +148 -3
- package/src/__tests__/assistant-attachments.test.ts +62 -4
- package/src/__tests__/assistant-id-boundary-guard.test.ts +0 -1
- package/src/__tests__/attachments-store-heic-normalize.test.ts +230 -0
- package/src/__tests__/attachments-store.test.ts +3 -0
- package/src/__tests__/background-shell-bash.test.ts +34 -0
- package/src/__tests__/background-shell-host-bash.test.ts +12 -0
- package/src/__tests__/background-tool-registry.test.ts +97 -0
- package/src/__tests__/background-tool-routes.test.ts +72 -1
- package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
- package/src/__tests__/btw-routes.test.ts +57 -3
- package/src/__tests__/call-controller.test.ts +474 -84
- package/src/__tests__/call-domain.test.ts +48 -1
- package/src/__tests__/call-pointer-messages.test.ts +57 -19
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +2 -6
- package/src/__tests__/call-routes-http.test.ts +15 -14
- package/src/__tests__/call-setup-flow-invite.test.ts +568 -0
- package/src/__tests__/call-setup-flow-midcall-trust.test.ts +231 -0
- package/src/__tests__/call-setup-flow-name-capture.test.ts +754 -0
- package/src/__tests__/call-setup-flow-verification.test.ts +925 -0
- package/src/__tests__/call-setup-flow.test.ts +311 -0
- package/src/__tests__/cancel-resolves-conversation-key.test.ts +0 -1
- package/src/__tests__/channel-guardian.test.ts +4 -7
- package/src/__tests__/channel-readiness-service.test.ts +0 -6
- package/src/__tests__/channel-readiness-slack-remote.test.ts +0 -4
- package/src/__tests__/channel-retry-sweep.test.ts +76 -64
- package/src/__tests__/channel-setup-panel-ack.test.ts +184 -0
- package/src/__tests__/clear-all-lexical.test.ts +90 -0
- package/src/__tests__/compactor-media-strip.test.ts +177 -0
- package/src/__tests__/config-loader-backfill.test.ts +5 -5
- package/src/__tests__/config-schema.test.ts +17 -0
- package/src/__tests__/contact-store-interaction-info.test.ts +27 -70
- package/src/__tests__/contacts-relay-reads.test.ts +188 -23
- package/src/__tests__/contacts-tools.test.ts +42 -1
- package/src/__tests__/contacts-write.test.ts +181 -0
- package/src/__tests__/context-search-conversations-source.test.ts +220 -81
- package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop.test.ts +39 -0
- package/src/__tests__/conversation-analysis-routes.test.ts +1 -1
- package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -0
- package/src/__tests__/conversation-clear-safety.test.ts +0 -1
- package/src/__tests__/conversation-crud-enabled-plugins.test.ts +60 -0
- package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +0 -1
- package/src/__tests__/conversation-disk-view.test.ts +1 -0
- package/src/__tests__/conversation-enabledplugins-route.test.ts +184 -0
- package/src/__tests__/conversation-error.test.ts +0 -18
- package/src/__tests__/conversation-fork-crud.test.ts +67 -0
- package/src/__tests__/conversation-history-web-search.test.ts +7 -137
- package/src/__tests__/conversation-initial-prompt.test.ts +110 -0
- package/src/__tests__/conversation-loop-db-lock-resilience.test.ts +191 -0
- package/src/__tests__/conversation-process-callsite.test.ts +37 -0
- package/src/__tests__/conversation-queue.test.ts +149 -0
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +392 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +84 -0
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +407 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +36 -16
- package/src/__tests__/conversation-skill-tools.test.ts +73 -0
- package/src/__tests__/conversation-wipe.test.ts +20 -5
- package/src/__tests__/credential-prompt-route.test.ts +69 -0
- package/src/__tests__/credential-security-invariants.test.ts +1 -3
- package/src/__tests__/db-drop-messages-fts.test.ts +115 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -62
- package/src/__tests__/edit-propagation.test.ts +56 -2
- package/src/__tests__/empty-state-greeting-cache.test.ts +9 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +15 -110
- package/src/__tests__/guardian-expiry-notifier.test.ts +1 -0
- package/src/__tests__/guardian-question-mode.test.ts +57 -0
- package/src/__tests__/guardian-verification-voice-binding.test.ts +1 -1
- package/src/__tests__/guardian-wait-controller.test.ts +619 -0
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +33 -2
- package/src/__tests__/heic-fixture.ts +55 -0
- package/src/__tests__/helpers/seed-contact-channel.ts +0 -2
- package/src/__tests__/http-conversation-lineage.test.ts +6 -1
- package/src/__tests__/image-conversion.test.ts +170 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +72 -341
- package/src/__tests__/introduction-card-resolver.test.ts +354 -0
- package/src/__tests__/invite-redeemed-ipc.test.ts +97 -0
- package/src/__tests__/invite-routes-http.test.ts +246 -420
- package/src/__tests__/job-handler-registration-guard.test.ts +104 -0
- package/src/__tests__/lexical-index-dual-write.test.ts +426 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +2 -2
- package/src/__tests__/list-messages-attachments.test.ts +159 -0
- package/src/__tests__/list-messages-background-tool-completion.test.ts +141 -0
- package/src/__tests__/list-messages-queued.test.ts +25 -0
- package/src/__tests__/managed-store.test.ts +129 -1
- package/src/__tests__/media-stream-audio-transcode.test.ts +113 -0
- package/src/__tests__/media-stream-output.test.ts +350 -19
- package/src/__tests__/media-stream-server-integration.test.ts +943 -169
- package/src/__tests__/media-stream-stt-session.test.ts +498 -1
- package/src/__tests__/memory-jobs-worker-lanes.test.ts +2 -2
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/mtime-cache.test.ts +19 -0
- package/src/__tests__/non-member-access-request.test.ts +349 -3
- package/src/__tests__/notification-decision-fallback.test.ts +104 -8
- package/src/__tests__/notification-decision-strategy.test.ts +57 -8
- package/src/__tests__/onboarding-template-contract.test.ts +27 -43
- package/src/__tests__/openai-provider.test.ts +33 -0
- package/src/__tests__/openai-responses-provider.test.ts +40 -0
- package/src/__tests__/persistence-layering-guard.test.ts +29 -20
- package/src/__tests__/persona-resolver.test.ts +56 -2
- package/src/__tests__/personal-memory-auth-bypass.test.ts +62 -0
- package/src/__tests__/phone.test.ts +1 -15
- package/src/__tests__/plugin-bootstrap.test.ts +25 -196
- package/src/__tests__/plugin-disabled-state.test.ts +33 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +227 -0
- package/src/__tests__/plugin-event-hub-facade.test.ts +323 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +156 -42
- package/src/__tests__/plugin-injector-contribution.test.ts +4 -20
- package/src/__tests__/plugin-pipeline.test.ts +63 -1
- package/src/__tests__/plugin-route-contribution.test.ts +62 -115
- package/src/__tests__/plugin-tool-contribution.test.ts +0 -22
- package/src/__tests__/plugin-types.test.ts +0 -1
- package/src/__tests__/pointer-conversation-trust.test.ts +38 -6
- package/src/__tests__/pre-model-call-sanitize.test.ts +1 -1
- package/src/__tests__/prune-old-tool-invocations-job.test.ts +97 -0
- package/src/__tests__/public-ingress-urls.test.ts +0 -48
- package/src/__tests__/qdrant-collection-migration.test.ts +145 -2
- package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +0 -1
- package/src/__tests__/resolve-trust-class.test.ts +28 -10
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +137 -0
- package/src/__tests__/schedule-retry.test.ts +38 -14
- package/src/__tests__/schedule-routes-workflow-validation.test.ts +3 -3
- package/src/__tests__/schedule-routes.test.ts +14 -6
- package/src/__tests__/schedule-store.test.ts +3 -1
- package/src/__tests__/scheduler-disk-pressure.test.ts +17 -6
- package/src/__tests__/scheduler-recurrence.test.ts +57 -107
- package/src/__tests__/scheduler-reuse-conversation.test.ts +51 -46
- package/src/__tests__/scheduler-wake.test.ts +13 -23
- package/src/__tests__/scheduler-worker-standdown.test.ts +220 -0
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/shell-observability.test.ts +1 -0
- package/src/__tests__/skill-boundary-guard.test.ts +3 -4
- package/src/__tests__/skill-load-plugin-scope.test.ts +238 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +2 -0
- package/src/__tests__/skill-script-runner-host.test.ts +53 -0
- package/src/__tests__/skill-script-runner-sandbox.test.ts +5 -2
- package/src/__tests__/skill-tool-factory.test.ts +73 -1
- package/src/__tests__/slack-app-setup-skill-regression.test.ts +37 -14
- package/src/__tests__/slack-inbound-verification.test.ts +36 -0
- package/src/__tests__/slack-notification-approval-card.test.ts +89 -2
- package/src/__tests__/slack-reaction-canonical-approval.test.ts +51 -1
- package/src/__tests__/stt-hints.test.ts +2 -256
- package/src/__tests__/subagent-allowlist-validation.test.ts +47 -0
- package/src/__tests__/subagent-call-site-routing.test.ts +72 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +14 -2
- package/src/__tests__/subagent-persistence.test.ts +122 -0
- package/src/__tests__/subagent-spawn-and-await.test.ts +1 -0
- package/src/__tests__/subagent-terminal-message.test.ts +99 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +78 -1
- package/src/__tests__/system-prompt.test.ts +17 -118
- package/src/__tests__/task-scheduler.test.ts +68 -89
- package/src/__tests__/telephony-credential-preflight.test.ts +312 -0
- package/src/__tests__/telephony-tts-guard.test.ts +538 -0
- package/src/__tests__/title-generate-hook.test.ts +16 -0
- package/src/__tests__/tool-approval-handler.test.ts +87 -1
- package/src/__tests__/tool-approval-seed-content-blocks.test.ts +45 -20
- package/src/__tests__/tool-grant-request-escalation.test.ts +39 -0
- package/src/__tests__/trust-context-guards.test.ts +21 -10
- package/src/__tests__/trusted-contact-multichannel.test.ts +4 -0
- package/src/__tests__/twilio-provider.test.ts +17 -17
- package/src/__tests__/twilio-routes-twiml.test.ts +6 -543
- package/src/__tests__/twilio-routes.test.ts +257 -387
- package/src/__tests__/twilio-validation.test.ts +1 -1
- package/src/__tests__/unified-turn-context-client-os.test.ts +45 -0
- package/src/__tests__/usage-cache-backfill-migration.test.ts +5 -0
- package/src/__tests__/voice-approval-resolver-relay.test.ts +150 -0
- package/src/__tests__/voice-ingress-preflight.test.ts +61 -0
- package/src/__tests__/voice-quality.test.ts +2 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +1 -7
- package/src/__tests__/voice-session-bridge-processing-wait.test.ts +27 -0
- package/src/__tests__/voice-session-bridge.test.ts +1 -10
- package/src/__tests__/workspace-git-service.test.ts +1 -1
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +1 -0
- package/src/__tests__/workspace-migration-118-seed-now-md.test.ts +120 -0
- package/src/__tests__/workspace-migration-120-revise-onboarding-threads.test.ts +175 -0
- package/src/__tests__/workspace-migration-121-seed-default-user-guardrails.test.ts +118 -0
- package/src/__tests__/workspace-migration-122-relocate-default-user-boundary.test.ts +119 -0
- package/src/__tests__/workspace-migration-123-swap-quality-profile-to-fable.test.ts +191 -0
- package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +235 -241
- package/src/acp/__tests__/prepare-agent-env.test.ts +0 -97
- package/src/acp/__tests__/session-manager-persistence.test.ts +2 -0
- package/src/acp/__tests__/session-manager-resume.test.ts +8 -0
- package/src/acp/__tests__/session-manager-usage.test.ts +6 -0
- package/src/acp/__tests__/session-manager.test.ts +6 -0
- package/src/acp/agent-process.test.ts +116 -0
- package/src/acp/agent-process.ts +71 -0
- package/src/acp/auto-install.ts +12 -8
- package/src/acp/failure-error.test.ts +109 -0
- package/src/acp/failure-error.ts +111 -0
- package/src/acp/prepare-agent-env.ts +0 -12
- package/src/acp/resolve-agent.test.ts +6 -45
- package/src/acp/resolve-agent.ts +2 -4
- package/src/acp/session-manager.test.ts +312 -0
- package/src/acp/session-manager.ts +102 -46
- package/src/agent/ax-tree-compaction.test.ts +4 -1
- package/src/agent/image-optimize.ts +17 -127
- package/src/agent/loop.ts +45 -192
- package/src/api/events/background-tool-completed.test.ts +51 -0
- package/src/api/events/background-tool-completed.ts +29 -0
- package/src/api/events/background-tool-started.test.ts +36 -0
- package/src/api/events/background-tool-started.ts +28 -0
- package/src/api/events/conversation-error.ts +0 -1
- package/src/api/events/open-panel.ts +33 -0
- package/src/api/events/subagent-status-changed.ts +4 -1
- package/src/api/index.ts +19 -0
- package/src/api/responses/conversation-message.ts +34 -0
- package/src/approvals/guardian-decision-primitive.ts +74 -24
- package/src/approvals/guardian-request-resolvers.ts +450 -122
- package/src/apps/app-store.ts +1 -0
- package/src/apps/shared-app-links-store.ts +1 -0
- package/src/avatar/identity-avatar.ts +3 -7
- package/src/background-wake/wake-intent-hooks.test.ts +1 -1
- package/src/calls/__tests__/fish-audio-client.test.ts +73 -0
- package/src/calls/__tests__/gateway-invite-reader.test.ts +170 -0
- package/src/calls/__tests__/relay-setup-router.test.ts +126 -102
- package/src/calls/call-controller.ts +134 -48
- package/src/calls/call-domain.ts +18 -42
- package/src/calls/call-pointer-messages.ts +51 -67
- package/src/calls/call-setup-flow-types.ts +76 -0
- package/src/calls/call-setup-flow.ts +1743 -0
- package/src/calls/call-speech-output.ts +87 -23
- package/src/calls/call-store.ts +4 -0
- package/src/calls/call-transport.ts +22 -36
- package/src/calls/fish-audio-client.ts +3 -0
- package/src/calls/gateway-invite-reader.ts +98 -0
- package/src/calls/guardian-wait-controller.ts +451 -0
- package/src/calls/media-stream-audio-transcode.ts +82 -2
- package/src/calls/media-stream-output.ts +142 -36
- package/src/calls/media-stream-server.ts +297 -178
- package/src/calls/media-stream-stt-session.ts +393 -50
- package/src/calls/media-turn-detector.ts +4 -6
- package/src/calls/relay-access-wait.ts +3 -4
- package/src/calls/relay-setup-router.ts +22 -43
- package/src/calls/relay-verification.ts +17 -24
- package/src/calls/resolve-call-tts-provider.ts +96 -29
- package/src/calls/stt-hints.ts +24 -87
- package/src/calls/telephony-credential-preflight.ts +159 -0
- package/src/calls/telephony-tts-capability.ts +183 -0
- package/src/calls/tts-call-strategy.ts +18 -20
- package/src/calls/twilio-provider.ts +3 -2
- package/src/calls/twilio-routes.ts +47 -351
- package/src/calls/types.ts +5 -3
- package/src/calls/voice-ingress-preflight.ts +9 -0
- package/src/calls/voice-quality.ts +13 -18
- package/src/calls/voice-session-bridge.ts +25 -35
- package/src/channels/__tests__/types.test.ts +28 -0
- package/src/channels/channel-verification-sessions.ts +7 -2
- package/src/channels/config.ts +0 -42
- package/src/channels/slack-thread-store.ts +12 -0
- package/src/channels/types.ts +23 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -1
- package/src/cli/commands/__tests__/schedules.test.ts +83 -34
- package/src/cli/commands/__tests__/status.test.ts +42 -2
- package/src/cli/commands/contacts.ts +2 -2
- package/src/cli/commands/credentials.ts +17 -0
- package/src/cli/commands/monitoring.ts +191 -0
- package/src/cli/commands/schedules-worker.ts +175 -0
- package/src/cli/commands/schedules.ts +169 -43
- package/src/cli/commands/status.ts +12 -1
- package/src/cli/commands/tools.ts +5 -1
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +1 -0
- package/src/cli/lib/__tests__/search-plugins.test.ts +29 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +7 -0
- package/src/cli/lib/search-plugins.ts +6 -0
- package/src/cli/lib/toggle-plugin.ts +21 -2
- package/src/cli/program.ts +3 -1
- package/src/config/__tests__/deployment-context-defaults.test.ts +23 -0
- package/src/config/acp-defaults.test.ts +0 -10
- package/src/config/acp-defaults.ts +0 -6
- package/src/config/bundled-skills/acp/SKILL.md +13 -45
- package/src/config/bundled-skills/acp/TOOLS.json +3 -3
- package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
- package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +9 -9
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +1 -1
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/references/CONFIG.md +9 -9
- package/src/config/bundled-skills/phone-calls/references/TROUBLESHOOTING.md +10 -7
- package/src/config/bundled-skills/skill-management/TOOLS.json +10 -0
- package/src/config/call-site-defaults.ts +0 -2
- package/src/config/feature-flag-registry.json +16 -8
- package/src/config/loader.ts +11 -0
- package/src/config/schema.ts +9 -1
- package/src/config/schemas/__tests__/memory-v2.test.ts +2 -2
- package/src/config/schemas/__tests__/memory-v3.test.ts +64 -0
- package/src/config/schemas/call-site-catalog.ts +0 -12
- package/src/config/schemas/calls.ts +8 -0
- package/src/config/schemas/elevenlabs.ts +1 -1
- package/src/config/schemas/llm.ts +0 -2
- package/src/config/schemas/memory-v2.ts +1 -1
- package/src/config/schemas/memory-v3.ts +82 -0
- package/src/config/schemas/monitoring.ts +61 -0
- package/src/config/schemas/schedules.ts +35 -0
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/skills-plugin-filter.test.ts +96 -0
- package/src/config/skills.ts +26 -0
- package/src/contacts/__tests__/member-write-relay.test.ts +156 -4
- package/src/contacts/canonical-guardian-store.ts +5 -0
- package/src/contacts/contact-store.ts +81 -35
- package/src/contacts/contacts-write.ts +50 -25
- package/src/contacts/member-write-relay.ts +115 -31
- package/src/contacts/types.ts +0 -8
- package/src/context/compactor.ts +29 -15
- package/src/context/outbound-sanitize.ts +202 -0
- package/src/daemon/__tests__/abort-null-controller.test.ts +167 -0
- package/src/daemon/__tests__/conversation-tool-setup-plugin-scope.test.ts +122 -0
- package/src/daemon/__tests__/embedding-reconcile.test.ts +99 -16
- package/src/daemon/__tests__/wake-conversation-ops-completion.test.ts +107 -0
- package/src/daemon/abort-watchdog.ts +7 -0
- package/src/daemon/app-source-watcher.ts +9 -0
- package/src/daemon/assistant-attachments.ts +42 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +256 -147
- package/src/daemon/conversation-agent-loop.ts +61 -68
- package/src/daemon/conversation-error.ts +1 -15
- package/src/daemon/conversation-history.ts +32 -236
- package/src/daemon/conversation-initial-prompt.ts +77 -0
- package/src/daemon/conversation-lifecycle.ts +25 -1
- package/src/daemon/conversation-messaging.ts +2 -2
- package/src/daemon/conversation-plugin-scope.ts +38 -0
- package/src/daemon/conversation-process.ts +50 -14
- package/src/daemon/conversation-runtime-assembly.ts +36 -6
- package/src/daemon/conversation-skill-tools.ts +19 -3
- package/src/daemon/conversation-store.ts +3 -3
- package/src/daemon/conversation-surfaces.ts +171 -0
- package/src/daemon/conversation-tool-setup.test.ts +95 -0
- package/src/daemon/conversation-tool-setup.ts +116 -1
- package/src/daemon/conversation-turn-finalize.ts +211 -0
- package/src/daemon/conversation.ts +92 -1
- package/src/daemon/disk-pressure-policy.ts +2 -1
- package/src/daemon/event-loop-watchdog.ts +22 -6
- package/src/daemon/external-plugins-bootstrap.ts +47 -134
- package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +1 -1
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +26 -14
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +27 -14
- package/src/daemon/handlers/__tests__/config-channels.test.ts +0 -5
- package/src/daemon/handlers/config-a2a.ts +7 -14
- package/src/daemon/handlers/config-channels.ts +2 -10
- package/src/daemon/handlers/conversation-history.ts +3 -1
- package/src/daemon/handlers/conversations.ts +0 -52
- package/src/daemon/handlers/shared.ts +4 -24
- package/src/daemon/handlers/skills.ts +4 -1
- package/src/daemon/host-cu-proxy.ts +1 -1
- package/src/daemon/lifecycle.ts +59 -472
- package/src/daemon/message-protocol.ts +3 -3
- package/src/daemon/message-provenance.ts +12 -14
- package/src/daemon/message-types/background-tools.ts +28 -0
- package/src/daemon/message-types/contacts.ts +2 -10
- package/src/daemon/message-types/conversations.ts +10 -0
- package/src/daemon/message-types/integrations.ts +2 -0
- package/src/daemon/message-types/surfaces.ts +1 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/pointer-conversation-trust.ts +23 -9
- package/src/daemon/pointer-turn-runner.ts +174 -0
- package/src/daemon/providers-setup.ts +16 -0
- package/src/daemon/shutdown-handlers.ts +139 -47
- package/src/daemon/skill-memory-refresh.ts +2 -2
- package/src/daemon/sqlite-corruption-watchdog.test.ts +217 -0
- package/src/daemon/sqlite-corruption-watchdog.ts +155 -0
- package/src/daemon/tool-setup-types.ts +8 -0
- package/src/daemon/trust-context.ts +21 -6
- package/src/daemon/turn-latency-tracker.test.ts +93 -0
- package/src/daemon/turn-latency-tracker.ts +38 -0
- package/src/daemon/wake-conversation-ops.ts +10 -8
- package/src/documents/document-comments-store.test.ts +7 -1
- package/src/documents/document-comments-store.ts +8 -0
- package/src/documents/document-store.ts +15 -0
- package/src/export/__tests__/transcript-formatter.test.ts +11 -0
- package/src/export/transcript-formatter.ts +49 -67
- package/src/home/__tests__/feed-writer.test.ts +247 -1
- package/src/home/conversation-starter-checkpoints.ts +1 -0
- package/src/home/feed-writer.ts +61 -0
- package/src/home/job-handlers/conversation-starters.ts +1 -0
- package/src/hooks/hook-loader.ts +12 -0
- package/src/hooks/registry.ts +32 -4
- package/src/inbound/public-ingress-urls.ts +0 -17
- package/src/ipc/__tests__/contacts-info-ipc.test.ts +285 -0
- package/src/ipc/assistant-server.ts +28 -7
- package/src/ipc/routes/__tests__/contacts-mirror-apply-atomicity.test.ts +62 -0
- package/src/ipc/routes/__tests__/contacts-mirror-apply.test.ts +272 -0
- package/src/ipc/routes/__tests__/contacts-mirror-ipc-routes.test.ts +322 -0
- package/src/ipc/routes/__tests__/invite-ipc-routes.test.ts +60 -4
- package/src/ipc/routes/contacts-info-ipc-routes.ts +239 -0
- package/src/ipc/routes/contacts-mirror-ipc-routes.ts +202 -0
- package/src/ipc/routes/db-proxy.ts +5 -2
- package/src/ipc/routes/invite-ipc-routes.ts +53 -39
- package/src/jobs/worker.ts +9 -2
- package/src/live-voice/__tests__/live-voice-archive.test.ts +10 -1
- package/src/live-voice/live-voice-archive.ts +5 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +22 -6
- package/src/messaging/providers/channel-transport.ts +6 -0
- package/src/messaging/providers/index.ts +3 -0
- package/src/messaging/providers/slack/api.test.ts +49 -1
- package/src/messaging/providers/slack/api.ts +110 -0
- package/src/messaging/providers/slack/send.test.ts +85 -0
- package/src/messaging/providers/slack/send.ts +100 -28
- package/src/messaging/providers/slack/transport.ts +7 -13
- package/src/messaging/providers/whatsapp/send.ts +29 -9
- package/src/monitoring/__tests__/control.test.ts +235 -0
- package/src/monitoring/__tests__/sample-ring-buffer.test.ts +96 -0
- package/src/monitoring/control.ts +105 -0
- package/src/monitoring/resource-sampler.ts +258 -0
- package/src/monitoring/sample-ring-buffer.ts +108 -0
- package/src/monitoring/worker.ts +94 -0
- package/src/notifications/README.md +1 -1
- package/src/notifications/access-request-copy.ts +94 -23
- package/src/notifications/adapters/slack.ts +89 -21
- package/src/notifications/approval-card-builder.ts +33 -18
- package/src/notifications/approval-card-data.ts +48 -15
- package/src/notifications/broadcaster.ts +25 -16
- package/src/notifications/decision-engine.ts +131 -32
- package/src/notifications/guardian-question-mode.ts +58 -10
- package/src/permissions/approval-policy.ts +8 -1
- package/src/permissions/checker.ts +2 -0
- package/src/permissions/gateway-threshold-reader.ts +7 -6
- package/src/permissions/ipc-risk-types.ts +1 -0
- package/src/permissions/secret-prompter.ts +11 -3
- package/src/permissions/types.ts +2 -2
- package/src/persistence/__tests__/conversation-queries-search.test.ts +397 -0
- package/src/persistence/__tests__/conversation-search-lexical.test.ts +103 -0
- package/src/persistence/__tests__/jobs-worker-memory-disabled.test.ts +95 -0
- package/src/persistence/__tests__/slow-query-log.test.ts +266 -0
- package/src/persistence/__tests__/slow-sync-log.test.ts +155 -0
- package/src/persistence/a2a-invite-store.ts +205 -0
- package/src/persistence/attachments-store.ts +94 -10
- package/src/persistence/checkpoints.ts +45 -0
- package/src/persistence/conversation-crud.ts +453 -260
- package/src/persistence/conversation-display-order-migration.ts +8 -2
- package/src/persistence/conversation-group-migration.test.ts +8 -0
- package/src/persistence/conversation-group-migration.ts +9 -0
- package/src/persistence/conversation-queries.ts +244 -125
- package/src/persistence/conversation-row-batch-delete.ts +9 -5
- package/src/persistence/conversation-search-lexical.ts +49 -0
- package/src/persistence/db-async-query.ts +42 -0
- package/src/persistence/db-connection.ts +13 -0
- package/src/persistence/delivery-channels.ts +0 -82
- package/src/persistence/delivery-crud.ts +27 -6
- package/src/persistence/embeddings/__tests__/embedding-identity.test.ts +18 -5
- package/src/persistence/embeddings/__tests__/messages-lexical-index.test.ts +334 -0
- package/src/persistence/embeddings/embedding-identity.ts +17 -5
- package/src/persistence/embeddings/embedding-runtime-manager.ts +31 -0
- package/src/persistence/embeddings/messages-lexical-index.ts +396 -0
- package/src/persistence/embeddings/qdrant-client.ts +81 -5
- package/src/persistence/group-crud.ts +13 -1
- package/src/persistence/job-handlers/__tests__/message-lexical-backfill.test.ts +506 -0
- package/src/persistence/job-handlers/__tests__/message-lexical-enqueue.test.ts +164 -0
- package/src/persistence/job-handlers/__tests__/message-lexical.test.ts +259 -0
- package/src/persistence/job-handlers/cleanup.ts +57 -9
- package/src/persistence/job-handlers/message-lexical-backfill.ts +187 -0
- package/src/persistence/job-handlers/message-lexical.ts +251 -0
- package/src/persistence/jobs-store.ts +102 -8
- package/src/persistence/jobs-worker.ts +68 -28
- package/src/persistence/llm-request-log-store.ts +35 -19
- package/src/persistence/llm-usage-store.ts +11 -0
- package/src/persistence/memory-lifecycle-hooks.test.ts +65 -0
- package/src/persistence/memory-lifecycle-hooks.ts +55 -1
- package/src/persistence/migrations/028-call-session-mode.ts +1 -1
- package/src/persistence/migrations/116-messages-fts.ts +8 -1
- package/src/persistence/migrations/311-create-subagents-table.ts +50 -0
- package/src/persistence/migrations/312-drop-inbox-conversation-state-table.ts +15 -0
- package/src/persistence/migrations/313-drop-messages-fts.ts +17 -0
- package/src/persistence/migrations/314-add-conversation-enabled-plugins.ts +25 -0
- package/src/persistence/migrations/315-create-a2a-invites.ts +58 -0
- package/src/persistence/migrations/316-drop-contact-channels-invite-id.ts +28 -0
- package/src/persistence/migrations/317-canonical-guardian-requester-signals.ts +24 -0
- package/src/persistence/migrations/318-drop-contact-channel-telemetry.ts +32 -0
- package/src/persistence/migrations/__tests__/315-create-a2a-invites.test.ts +161 -0
- package/src/persistence/migrations/__tests__/316-drop-contact-channels-invite-id.test.ts +156 -0
- package/src/persistence/migrations/__tests__/318-drop-contact-channel-telemetry.test.ts +134 -0
- package/src/persistence/migrations/__tests__/migration-prefix-guard.test.ts +99 -0
- package/src/persistence/raw-query.ts +50 -11
- package/src/persistence/schema/a2a.ts +14 -0
- package/src/persistence/schema/contacts.ts +0 -62
- package/src/persistence/schema/conversation-groups.ts +12 -0
- package/src/persistence/schema/conversations.ts +2 -0
- package/src/persistence/schema/documents.ts +65 -0
- package/src/persistence/schema/guardian.ts +4 -0
- package/src/persistence/schema/index.ts +4 -0
- package/src/persistence/schema/memory-injection.ts +62 -0
- package/src/persistence/schema/workflows.ts +55 -0
- package/src/persistence/slow-query-log.ts +437 -0
- package/src/persistence/slow-sync-log.ts +204 -0
- package/src/persistence/steps.ts +16 -0
- package/src/persistence/subagent-store.ts +125 -0
- package/src/persistence/worker-control.ts +45 -238
- package/src/plugin-api/event-hub-facade.ts +172 -0
- package/src/plugin-api/index.ts +9 -1
- package/src/plugin-api/types.ts +40 -9
- package/src/plugins/defaults/default-plugin-names.ts +60 -0
- package/src/plugins/defaults/index.ts +19 -29
- package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +46 -67
- package/src/plugins/defaults/memory/__tests__/db-async-query.test.ts +36 -1
- package/src/plugins/defaults/memory/__tests__/find-most-recent-retrospective-for.test.ts +2 -4
- package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +2 -2
- package/src/plugins/defaults/memory/__tests__/jobs-worker-v2-schedule.test.ts +9 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +7 -4
- package/src/plugins/defaults/memory/config.ts +11 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -1
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +124 -113
- package/src/plugins/defaults/memory/embeddings.ts +77 -0
- package/src/plugins/defaults/memory/find-most-recent-retrospective-for.ts +66 -0
- package/src/plugins/defaults/memory/frontmatter.ts +50 -0
- package/src/plugins/defaults/memory/graph/bootstrap.test.ts +36 -10
- package/src/plugins/defaults/memory/graph/bootstrap.ts +6 -5
- package/src/plugins/defaults/memory/graph/consolidation.ts +3 -5
- package/src/plugins/defaults/memory/graph/extraction.ts +3 -6
- package/src/plugins/defaults/memory/graph/graph-search.ts +12 -23
- package/src/plugins/defaults/memory/graph/narrative.ts +3 -5
- package/src/plugins/defaults/memory/graph/pattern-scan.ts +3 -5
- package/src/plugins/defaults/memory/graph/retriever.test.ts +1 -6
- package/src/plugins/defaults/memory/graph/retriever.ts +5 -10
- package/src/plugins/defaults/memory/graph/tools.ts +1 -1
- package/src/plugins/defaults/memory/hooks/init.ts +31 -5
- package/src/plugins/defaults/memory/indexer.ts +2 -3
- package/src/plugins/defaults/memory/injectors.ts +2 -2
- package/src/plugins/defaults/memory/job-handler-registration.ts +96 -0
- package/src/plugins/defaults/memory/job-handlers/backfill.ts +4 -26
- package/src/plugins/defaults/memory/job-handlers/embedding.test.ts +6 -19
- package/src/plugins/defaults/memory/job-handlers/embedding.ts +9 -22
- package/src/plugins/defaults/memory/job-handlers/index-maintenance.ts +2 -3
- package/src/plugins/defaults/memory/job-handlers.ts +13 -12
- package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +1 -1
- package/src/plugins/defaults/memory/llm-helpers.ts +76 -0
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +8 -5
- package/src/plugins/defaults/memory/memory-retrospective-startup-cleanup.ts +2 -2
- package/src/plugins/defaults/memory/memory-v2-concept-frequency.ts +2 -0
- package/src/plugins/defaults/memory/persistence-hooks.ts +46 -1
- package/src/plugins/defaults/memory/pkb/pkb-index.ts +1 -5
- package/src/plugins/defaults/memory/pkb/pkb-search.ts +2 -2
- package/src/plugins/defaults/memory/routes/__tests__/memory-v2-simulate-route.test.ts +4 -6
- package/src/plugins/defaults/memory/routes/memory-item-routes.ts +1 -1
- package/src/plugins/defaults/memory/search/semantic.ts +2 -2
- package/src/plugins/defaults/memory/startup.ts +244 -0
- package/src/plugins/defaults/memory/task-memory-cleanup.ts +8 -0
- package/src/{daemon → plugins/defaults/memory/v2}/__tests__/memory-v2-startup.test.ts +6 -6
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +2 -13
- package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +4 -5
- package/src/plugins/defaults/memory/v2/__tests__/sweep-job.test.ts +1 -8
- package/src/plugins/defaults/memory/v2/activation.ts +2 -4
- package/src/plugins/defaults/memory/v2/cli-command-store.ts +2 -4
- package/src/plugins/defaults/memory/v2/frontmatter-sweep.ts +1 -1
- package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.test.ts +8 -8
- package/src/{daemon → plugins/defaults/memory/v2}/memory-v2-startup.ts +20 -22
- package/src/plugins/defaults/memory/v2/migration.ts +2 -5
- package/src/plugins/defaults/memory/v2/page-store.ts +1 -1
- package/src/plugins/defaults/memory/v2/qdrant.ts +2 -3
- package/src/plugins/defaults/memory/v2/router.ts +5 -5
- package/src/plugins/defaults/memory/v2/sim.ts +2 -4
- package/src/plugins/defaults/memory/v2/skill-store.ts +2 -4
- package/src/plugins/defaults/memory/v2/sweep-job.ts +6 -6
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +11 -9
- package/src/plugins/defaults/memory/v3/__tests__/dense.test.ts +107 -2
- package/src/plugins/defaults/memory/v3/__tests__/gate-flag.test.ts +69 -0
- package/src/plugins/defaults/memory/v3/__tests__/gate.test.ts +181 -0
- package/src/plugins/defaults/memory/v3/__tests__/live-integration.test.ts +7 -3
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +345 -8
- package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -4
- package/src/plugins/defaults/memory/v3/__tests__/section-needle.test.ts +64 -0
- package/src/plugins/defaults/memory/v3/__tests__/shadow-integration.test.ts +18 -13
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +62 -1
- package/src/plugins/defaults/memory/v3/card.ts +1 -4
- package/src/plugins/defaults/memory/v3/dense.ts +60 -25
- package/src/plugins/defaults/memory/v3/edge.ts +1 -1
- package/src/plugins/defaults/memory/v3/gate-flag.ts +20 -0
- package/src/plugins/defaults/memory/v3/gate.ts +158 -0
- package/src/plugins/defaults/memory/v3/maintain-job.ts +2 -4
- package/src/plugins/defaults/memory/v3/orchestrate.ts +186 -25
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +7 -9
- package/src/plugins/defaults/memory/v3/pool-select.ts +6 -6
- package/src/plugins/defaults/memory/v3/prune.ts +2 -2
- package/src/plugins/defaults/memory/v3/section-dense-store.ts +8 -11
- package/src/plugins/defaults/memory/v3/section-needle.ts +27 -7
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +10 -0
- package/src/plugins/defaults/memory/v3-eval/eval-packets.ts +1 -4
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +5 -0
- package/src/plugins/defaults/turn-context/injectors.ts +1 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +12 -0
- package/src/plugins/injector-registry.ts +13 -2
- package/src/plugins/mtime-cache.ts +36 -47
- package/src/plugins/pipeline.ts +15 -1
- package/src/plugins/types.ts +14 -23
- package/src/prompts/__tests__/system-prompt.test.ts +170 -32
- package/src/prompts/persona-resolver.ts +13 -9
- package/src/prompts/system-prompt.ts +30 -9
- package/src/prompts/templates/BOOTSTRAP.md +16 -119
- package/src/prompts/templates/SOUL.md +43 -13
- package/src/prompts/templates/system-sections.ts +32 -27
- package/src/prompts/templates/users/default.md +19 -0
- package/src/providers/__tests__/placeholder-sentinels.test.ts +56 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +96 -1
- package/src/providers/model-intents.ts +2 -2
- package/src/providers/openai/chat-completions-provider.ts +14 -1
- package/src/providers/openai/responses-provider.ts +18 -0
- package/src/providers/placeholder-sentinels.ts +18 -8
- package/src/providers/retry.ts +1 -0
- package/src/providers/speech-to-text/__tests__/provider-catalog.test.ts +0 -64
- package/src/providers/speech-to-text/__tests__/resolve.test.ts +92 -79
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +98 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +62 -3
- package/src/providers/speech-to-text/provider-catalog.ts +0 -89
- package/src/providers/speech-to-text/resolve.ts +55 -0
- package/src/providers/tool-progress-events.test.ts +50 -0
- package/src/providers/tool-progress-events.ts +99 -0
- package/src/providers/types.ts +1 -1
- package/src/providers/vellum-model-routing.test.ts +51 -0
- package/src/providers/vellum-model-routing.ts +84 -0
- package/src/runtime/AGENTS.md +5 -4
- package/src/runtime/__tests__/agent-wake.test.ts +158 -1
- package/src/runtime/__tests__/channel-verification-service.test.ts +2 -2
- package/src/runtime/__tests__/trust-verdict-consumer.test.ts +37 -8
- package/src/runtime/access-request-helper.ts +154 -3
- package/src/runtime/actor-trust-resolver.ts +7 -21
- package/src/runtime/agent-wake.ts +41 -1
- package/src/runtime/assistant-event.ts +86 -12
- package/src/runtime/auth/__tests__/route-policy.test.ts +31 -0
- package/src/runtime/channel-approval-types.ts +52 -2
- package/src/runtime/channel-approvals.ts +23 -8
- package/src/runtime/channel-invite-transport.ts +2 -2
- package/src/runtime/channel-invite-transports/voice.ts +1 -1
- package/src/runtime/channel-readiness-service.ts +3 -5
- package/src/runtime/channel-retry-sweep.ts +18 -24
- package/src/runtime/channel-verification-service.ts +9 -11
- package/src/runtime/finalize-event-delivery.ts +30 -16
- package/src/runtime/guardian-action-service.ts +10 -11
- package/src/runtime/guardian-reply-router.ts +122 -21
- package/src/runtime/http-server.ts +44 -101
- package/src/runtime/introduction-policy.test.ts +214 -0
- package/src/runtime/introduction-policy.ts +284 -0
- package/src/runtime/invite-service.ts +104 -361
- package/src/runtime/local-actor-identity.test.ts +38 -0
- package/src/runtime/local-actor-identity.ts +14 -0
- package/src/runtime/middleware/__tests__/rate-limiter.test.ts +22 -0
- package/src/runtime/middleware/rate-limiter.ts +30 -0
- package/src/runtime/middleware/twilio-validation.ts +5 -7
- package/src/runtime/migrations/__tests__/gcs-signed-url.test.ts +28 -0
- package/src/runtime/migrations/origin-mode.ts +6 -6
- package/src/runtime/pre-first-message-gate.ts +1 -0
- package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +6 -35
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +24 -3
- package/src/runtime/routes/__tests__/contact-routes.test.ts +80 -15
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +7 -1
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +0 -3
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +142 -1
- package/src/runtime/routes/__tests__/invite-relay-routes.test.ts +119 -30
- package/src/runtime/routes/__tests__/migration-import-allowed-hosts-env.test.ts +70 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +194 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +755 -29
- package/src/runtime/routes/__tests__/retrospective-routes.test.ts +4 -0
- package/src/runtime/routes/__tests__/schedule-routes-create.test.ts +70 -0
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +161 -0
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +1 -1
- package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -1
- package/src/runtime/routes/acp-routes.ts +15 -3
- package/src/runtime/routes/app-management-routes.ts +4 -7
- package/src/runtime/routes/approval-routes.ts +4 -0
- package/src/runtime/routes/attachment-routes.ts +54 -3
- package/src/runtime/routes/avatar-routes.ts +0 -4
- package/src/runtime/routes/background-tool-routes.ts +34 -5
- package/src/runtime/routes/btw-routes.ts +32 -4
- package/src/runtime/routes/channel-route-shared.ts +18 -14
- package/src/runtime/routes/channel-verification-routes.ts +0 -3
- package/src/runtime/routes/contact-routes.ts +298 -117
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +74 -37
- package/src/runtime/routes/conversation-query-routes.ts +5 -2
- package/src/runtime/routes/conversation-routes.ts +222 -82
- package/src/runtime/routes/conversations-import-routes.ts +8 -1
- package/src/runtime/routes/credential-prompt-routes.ts +20 -5
- package/src/runtime/routes/debug-routes.ts +1 -0
- package/src/runtime/routes/documents-routes.ts +5 -2
- package/src/runtime/routes/empty-state-greeting-cache.ts +37 -10
- package/src/runtime/routes/global-search-routes.ts +2 -1
- package/src/runtime/routes/guardian-approval-interception.ts +51 -0
- package/src/runtime/routes/home-feed-routes.ts +60 -1
- package/src/runtime/routes/identity-routes.ts +4 -87
- package/src/runtime/routes/inbound-message-handler.ts +82 -41
- package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +430 -57
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +302 -572
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +76 -270
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +21 -116
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +9 -1
- package/src/runtime/routes/inbound-stages/escalation-intercept.test.ts +158 -0
- package/src/runtime/routes/inbound-stages/escalation-intercept.ts +22 -1
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +22 -16
- package/src/runtime/routes/internal-twilio-routes.ts +0 -15
- package/src/runtime/routes/messages-lexical-routes.ts +75 -0
- package/src/runtime/routes/migration-routes.ts +69 -6
- package/src/runtime/routes/monitoring-routes.ts +237 -0
- package/src/runtime/routes/plugins-routes.ts +329 -8
- package/src/runtime/routes/schedule-routes.ts +49 -8
- package/src/runtime/routes/schedule-worker-routes.ts +210 -0
- package/src/runtime/routes/secret-routes.ts +1 -1
- package/src/runtime/routes/surface-conversation-resolver.ts +1 -0
- package/src/runtime/services/__tests__/analyze-conversation.test.ts +54 -9
- package/src/runtime/services/__tests__/auto-analysis-enqueue.test.ts +30 -6
- package/src/runtime/services/__tests__/conversation-analyze-job.test.ts +43 -8
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +20 -0
- package/src/runtime/services/analyze-conversation.ts +14 -4
- package/src/runtime/services/conversation-serializer.ts +5 -0
- package/src/runtime/slack-no-response.ts +47 -0
- package/src/runtime/slack-reply-session.test.ts +633 -0
- package/src/runtime/slack-reply-session.ts +382 -0
- package/src/runtime/slack-task-progress.ts +95 -0
- package/src/runtime/sync/resource-sync-events.ts +25 -0
- package/src/runtime/tool-grant-request-helper.ts +17 -1
- package/src/runtime/trust-class.test.ts +141 -0
- package/src/runtime/trust-class.ts +121 -0
- package/src/runtime/trust-verdict-consumer.ts +7 -6
- package/src/schedule/__tests__/run-script.test.ts +21 -0
- package/src/schedule/__tests__/worker-control.test.ts +241 -0
- package/src/schedule/run-script.ts +13 -6
- package/src/schedule/schedule-usage-store.ts +1 -0
- package/src/schedule/scheduler.ts +178 -127
- package/src/schedule/worker-control.ts +118 -0
- package/src/schedule/worker.ts +131 -0
- package/src/skills/__tests__/categories-cache.test.ts +38 -0
- package/src/skills/categories-cache.ts +40 -5
- package/src/skills/managed-store.ts +41 -6
- package/src/skills/validate-input.ts +47 -0
- package/src/stt/types.ts +5 -9
- package/src/subagent/manager.ts +267 -39
- package/src/subagent/types.ts +6 -2
- package/src/subagent/validate-allowlists.ts +65 -0
- package/src/telemetry/watchdog-direct-emit.test.ts +109 -0
- package/src/telemetry/watchdog-direct-emit.ts +89 -0
- package/src/tools/acp/list-agents.test.ts +0 -6
- package/src/tools/background-tool-registry.ts +61 -0
- package/src/tools/document/document-comment-tool.test.ts +4 -0
- package/src/tools/host-terminal/host-shell.test.ts +397 -0
- package/src/tools/host-terminal/host-shell.ts +203 -14
- package/src/tools/skills/find-similar-skills.test.ts +152 -1
- package/src/tools/skills/find-similar-skills.ts +30 -4
- package/src/tools/skills/load.ts +36 -0
- package/src/tools/skills/scaffold-managed.ts +66 -34
- package/src/tools/skills/skill-script-runner.ts +17 -0
- package/src/tools/skills/skill-tool-factory.ts +5 -1
- package/src/tools/terminal/safe-env.ts +3 -0
- package/src/tools/terminal/shell.test.ts +177 -0
- package/src/tools/terminal/shell.ts +98 -4
- package/src/tools/tool-approval-handler.ts +106 -29
- package/{node_modules/@vellumai/skill-host-contracts/src → src/tools}/tool-types.ts +16 -17
- package/src/tools/types.ts +29 -17
- package/src/tools/ui-surface/definitions.ts +3 -1
- package/src/tts/__tests__/provider-adapters.test.ts +37 -0
- package/src/tts/__tests__/provider-catalog.test.ts +21 -0
- package/src/tts/provider-catalog.ts +40 -9
- package/src/tts/providers/fish-audio-provider.ts +19 -10
- package/src/tts/types.ts +3 -4
- package/src/util/__tests__/process-tree.test.ts +10 -2
- package/src/util/__tests__/text-spacing.test.ts +53 -0
- package/src/util/cgroup-memory.ts +142 -0
- package/src/util/image-conversion.ts +281 -0
- package/src/util/phone.ts +1 -8
- package/src/util/platform.ts +33 -0
- package/src/util/process-tree.ts +10 -2
- package/src/util/text-spacing.ts +53 -0
- package/src/util/worker-process.ts +271 -0
- package/src/workflows/journal-store.ts +16 -1
- package/src/workflows/library.test.ts +35 -20
- package/src/workflows/library.ts +105 -66
- package/src/workspace/git-service.ts +1 -0
- package/src/workspace/migrations/011-backfill-installation-id.ts +83 -31
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +41 -22
- package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +1 -1
- package/src/workspace/migrations/118-seed-now-md.ts +64 -0
- package/src/workspace/migrations/119-strip-persisted-memory-v3-tuning-defaults.ts +169 -0
- package/src/workspace/migrations/120-revise-onboarding-threads.ts +47 -0
- package/src/workspace/migrations/121-seed-default-user-guardrails.ts +82 -0
- package/src/workspace/migrations/122-relocate-default-user-boundary.ts +111 -0
- package/src/workspace/migrations/123-swap-quality-profile-to-fable.ts +85 -0
- package/src/workspace/migrations/__tests__/119-strip-persisted-memory-v3-tuning-defaults.test.ts +181 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/drizzle/0000_dizzy_maggott.sql +0 -301
- package/drizzle/meta/0000_snapshot.json +0 -1933
- package/drizzle/meta/_journal.json +0 -13
- package/node_modules/@vellumai/skill-host-contracts/__tests__/client.test.ts +0 -887
- package/node_modules/@vellumai/skill-host-contracts/bun.lock +0 -24
- package/node_modules/@vellumai/skill-host-contracts/package.json +0 -18
- package/node_modules/@vellumai/skill-host-contracts/src/assistant-event.ts +0 -97
- package/node_modules/@vellumai/skill-host-contracts/src/client.ts +0 -1349
- package/node_modules/@vellumai/skill-host-contracts/src/index.ts +0 -6
- package/node_modules/@vellumai/skill-host-contracts/src/runtime-mode.ts +0 -11
- package/node_modules/@vellumai/skill-host-contracts/src/server-message.ts +0 -32
- package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +0 -333
- package/node_modules/@vellumai/skill-host-contracts/tsconfig.json +0 -20
- package/node_modules/@vellumai/skill-host-contracts/tsconfig.test.json +0 -12
- package/src/__tests__/invite-redemption-service.test.ts +0 -1180
- package/src/__tests__/invite-service-ipc.test.ts +0 -350
- package/src/__tests__/job-handler-registry-guard.test.ts +0 -125
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +0 -203
- package/src/__tests__/relay-server.test.ts +0 -6153
- package/src/__tests__/telephony-stt-routing.test.ts +0 -329
- package/src/__tests__/voice-invite-redemption.test.ts +0 -702
- package/src/calls/relay-server.ts +0 -2463
- package/src/calls/telephony-stt-routing.ts +0 -145
- package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +0 -128
- package/src/daemon/__tests__/daemon-skill-host.test.ts +0 -273
- package/src/daemon/daemon-skill-host.ts +0 -262
- package/src/daemon/message-types/meet.ts +0 -143
- package/src/daemon/shutdown-registry.ts +0 -40
- package/src/ipc/__tests__/skill-server-bidirectional.test.ts +0 -253
- package/src/ipc/__tests__/skill-server.test.ts +0 -182
- package/src/ipc/skill-ipc-types.ts +0 -54
- package/src/ipc/skill-routes/__tests__/config.test.ts +0 -146
- package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +0 -448
- package/src/ipc/skill-routes/__tests__/identity.test.ts +0 -62
- package/src/ipc/skill-routes/__tests__/log.test.ts +0 -133
- package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -203
- package/src/ipc/skill-routes/__tests__/platform.test.ts +0 -111
- package/src/ipc/skill-routes/__tests__/providers.test.ts +0 -265
- package/src/ipc/skill-routes/__tests__/registries.test.ts +0 -400
- package/src/ipc/skill-routes/config.ts +0 -47
- package/src/ipc/skill-routes/events.ts +0 -147
- package/src/ipc/skill-routes/identity.ts +0 -21
- package/src/ipc/skill-routes/index.ts +0 -37
- package/src/ipc/skill-routes/log.ts +0 -40
- package/src/ipc/skill-routes/memory.ts +0 -92
- package/src/ipc/skill-routes/platform.ts +0 -39
- package/src/ipc/skill-routes/providers.ts +0 -162
- package/src/ipc/skill-routes/registries.ts +0 -338
- package/src/ipc/skill-server.ts +0 -831
- package/src/ipc/skill-socket-path.ts +0 -20
- package/src/jobs/register-job-handlers.ts +0 -74
- package/src/persistence/invite-store.ts +0 -481
- package/src/plugins/job-handler-registry.ts +0 -94
- package/src/providers/cache-control.ts +0 -26
- package/src/runtime/__tests__/runtime-mode.test.ts +0 -62
- package/src/runtime/invite-redemption-service.ts +0 -828
- package/src/runtime/invite-redemption-templates.ts +0 -46
- package/src/runtime/runtime-mode.ts +0 -40
- package/src/runtime/slack-dm-text-delivery.ts +0 -177
- package/src/util/voice-code.ts +0 -31
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ingress ACL enforcement stage: resolves the inbound actor to a member
|
|
3
|
-
* record, enforces allow/deny/escalate policies,
|
|
4
|
-
*
|
|
3
|
+
* record, enforces allow/deny/escalate policies, and notifies the guardian
|
|
4
|
+
* of denied access requests.
|
|
5
|
+
*
|
|
6
|
+
* Invite code/token redemption is intercepted at gateway ingress; redeemed
|
|
7
|
+
* messages never reach this stage.
|
|
5
8
|
*/
|
|
6
9
|
import type { AdmissionPolicy, SourceMetadata } from "@vellumai/gateway-client";
|
|
10
|
+
import { isTrustClass } from "@vellumai/gateway-client";
|
|
7
11
|
|
|
8
|
-
import { isInviteCodeRedemptionEnabled } from "../../../channels/config.js";
|
|
9
12
|
import type { ChannelId } from "../../../channels/types.js";
|
|
10
13
|
import { getGuardianDelivery } from "../../../contacts/guardian-delivery-reader.js";
|
|
11
14
|
import { channelStatusToMemberStatus } from "../../../contacts/member-status.js";
|
|
12
15
|
import { MESSAGE_PREVIEW_MAX_LENGTH } from "../../../notifications/notification-utils.js";
|
|
13
|
-
import {
|
|
14
|
-
deleteInbound,
|
|
15
|
-
recordInbound,
|
|
16
|
-
} from "../../../persistence/delivery-crud.js";
|
|
17
|
-
import { markProcessed } from "../../../persistence/delivery-status.js";
|
|
18
|
-
import {
|
|
19
|
-
findByInviteCodeHash,
|
|
20
|
-
findByInviteCodeHashAnyChannel,
|
|
21
|
-
} from "../../../persistence/invite-store.js";
|
|
22
16
|
import { resolveGuardianName } from "../../../prompts/user-reference.js";
|
|
23
17
|
import { getLogger } from "../../../util/logger.js";
|
|
24
18
|
import { truncate } from "../../../util/truncate.js";
|
|
25
|
-
import {
|
|
26
|
-
|
|
19
|
+
import {
|
|
20
|
+
isAccessRequestDenied,
|
|
21
|
+
isApprovalHandshakeInProgress,
|
|
22
|
+
notifyGuardianOfAccessRequest,
|
|
23
|
+
} from "../../access-request-helper.js";
|
|
27
24
|
import { resolveAnchoredGuardian } from "../../anchored-guardian.js";
|
|
28
|
-
import { getInviteAdapterRegistry } from "../../channel-invite-transport.js";
|
|
29
25
|
import {
|
|
30
26
|
createOutboundSession,
|
|
31
27
|
findActiveSession,
|
|
@@ -33,11 +29,6 @@ import {
|
|
|
33
29
|
resolveBootstrapToken,
|
|
34
30
|
} from "../../channel-verification-service.js";
|
|
35
31
|
import { deliverChannelReply } from "../../gateway-client.js";
|
|
36
|
-
import {
|
|
37
|
-
redeemInvite,
|
|
38
|
-
redeemInviteByCode,
|
|
39
|
-
} from "../../invite-redemption-service.js";
|
|
40
|
-
import { getInviteRedemptionReply } from "../../invite-redemption-templates.js";
|
|
41
32
|
import type { VerdictMember } from "../../trust-verdict-consumer.js";
|
|
42
33
|
import { verdictMemberFromVerdict } from "../../trust-verdict-consumer.js";
|
|
43
34
|
|
|
@@ -63,6 +54,32 @@ async function resolveGuardianLabel(sourceChannel: ChannelId): Promise<string> {
|
|
|
63
54
|
return resolveGuardianName(anchored?.displayName);
|
|
64
55
|
}
|
|
65
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Compose the requester-facing reply for a denied inbound, keyed on the
|
|
59
|
+
* access-request outcome. Single source for this copy — the not_a_member,
|
|
60
|
+
* inactive-member, and admission-floor deny lanes all route through it.
|
|
61
|
+
*
|
|
62
|
+
* - Handshake in progress: the guardian already approved and a verification
|
|
63
|
+
* code is live; tell the sender their next step. The code is DM'd directly
|
|
64
|
+
* to the requester on Slack but relayed via the guardian elsewhere, so the
|
|
65
|
+
* copy covers both.
|
|
66
|
+
* - Guardian notified: the standard "I'll let <guardian> know" copy.
|
|
67
|
+
* - Otherwise: the plain not-approved copy.
|
|
68
|
+
*/
|
|
69
|
+
export async function composeAccessDenialReply(params: {
|
|
70
|
+
sourceChannel: ChannelId;
|
|
71
|
+
guardianNotified: boolean;
|
|
72
|
+
handshakeInProgress: boolean;
|
|
73
|
+
}): Promise<string> {
|
|
74
|
+
if (params.handshakeInProgress) {
|
|
75
|
+
return `Your access request was approved! Reply here with the 6-digit verification code to finish connecting — if you don't have it, ask ${await resolveGuardianLabel(params.sourceChannel)} for it.`;
|
|
76
|
+
}
|
|
77
|
+
if (params.guardianNotified) {
|
|
78
|
+
return `Hmm looks like you don't have access to talk to me. I'll let ${await resolveGuardianLabel(params.sourceChannel)} know you tried talking to me and get back to you.`;
|
|
79
|
+
}
|
|
80
|
+
return "Sorry, you haven't been approved to message this assistant.";
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
// ---------------------------------------------------------------------------
|
|
67
84
|
// Public API
|
|
68
85
|
// ---------------------------------------------------------------------------
|
|
@@ -80,7 +97,6 @@ export interface AclEnforcementParams {
|
|
|
80
97
|
actorUsername: string | undefined;
|
|
81
98
|
replyCallbackUrl: string | undefined;
|
|
82
99
|
assistantId: string;
|
|
83
|
-
externalMessageId: string;
|
|
84
100
|
/**
|
|
85
101
|
* Effective admission policy for this request (gateway floor resolved with
|
|
86
102
|
* any per-conversation override). When set, ACL skips its hard-deny paths
|
|
@@ -93,6 +109,35 @@ export interface AclEnforcementParams {
|
|
|
93
109
|
* replies for senders who will be admitted by the floor stage anyway.
|
|
94
110
|
*/
|
|
95
111
|
effectiveAdmissionPolicy?: AdmissionPolicy;
|
|
112
|
+
/**
|
|
113
|
+
* True when the inbound event is an interaction callback (e.g. a Slack
|
|
114
|
+
* Block Kit button press or a message_deleted sentinel) rather than a
|
|
115
|
+
* message the sender composed. Callbacks are decision attempts / lifecycle
|
|
116
|
+
* events, not access attempts: a denied callback must never mint a
|
|
117
|
+
* verification challenge or create an access request — a stale button
|
|
118
|
+
* press from an unrecognized sender would otherwise spawn a fresh
|
|
119
|
+
* Approve/Reject card the guardian already dealt with (LUM-2673). The
|
|
120
|
+
* deny itself (and its canned reply) still applies. Required so every
|
|
121
|
+
* caller makes the classification explicitly.
|
|
122
|
+
*/
|
|
123
|
+
isCallbackInteraction: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fail-closed / fail-safe deny result: soft-deny with NO stranger-lane side
|
|
128
|
+
* effects (no access-request card, no verification challenge, no canned
|
|
129
|
+
* reply). Used for unresolvable verdicts, where the sender must not be
|
|
130
|
+
* treated as a stranger.
|
|
131
|
+
*/
|
|
132
|
+
function failClosedDeny(): AclResult {
|
|
133
|
+
return {
|
|
134
|
+
resolvedMember: null,
|
|
135
|
+
earlyResponse: {
|
|
136
|
+
accepted: true,
|
|
137
|
+
denied: true,
|
|
138
|
+
reason: "not_a_member",
|
|
139
|
+
},
|
|
140
|
+
};
|
|
96
141
|
}
|
|
97
142
|
|
|
98
143
|
export interface AclResult {
|
|
@@ -109,8 +154,8 @@ export interface AclResult {
|
|
|
109
154
|
|
|
110
155
|
/**
|
|
111
156
|
* Enforce ingress ACL rules: member lookup, non-member/inactive denial,
|
|
112
|
-
* policy enforcement (allow/deny/escalate bypass),
|
|
113
|
-
*
|
|
157
|
+
* policy enforcement (allow/deny/escalate bypass), and guardian
|
|
158
|
+
* notification for denied access.
|
|
114
159
|
*/
|
|
115
160
|
export async function enforceIngressAcl(
|
|
116
161
|
params: AclEnforcementParams,
|
|
@@ -128,13 +173,15 @@ export async function enforceIngressAcl(
|
|
|
128
173
|
actorUsername,
|
|
129
174
|
replyCallbackUrl,
|
|
130
175
|
assistantId,
|
|
131
|
-
externalMessageId,
|
|
132
176
|
effectiveAdmissionPolicy,
|
|
177
|
+
isCallbackInteraction,
|
|
133
178
|
} = params;
|
|
134
179
|
|
|
135
180
|
let isValidatedBootstrap = false;
|
|
136
181
|
|
|
137
|
-
//
|
|
182
|
+
// Identity signals forwarded via sourceMetadata: bot flag (Slack users.info
|
|
183
|
+
// / Telegram is_bot) plus Slack workspace trust signals.
|
|
184
|
+
const isBot = sourceMetadata?.isBot ?? undefined;
|
|
138
185
|
const isStranger = sourceMetadata?.isStranger ?? undefined;
|
|
139
186
|
const isRestricted = sourceMetadata?.isRestricted ?? undefined;
|
|
140
187
|
|
|
@@ -143,21 +190,14 @@ export async function enforceIngressAcl(
|
|
|
143
190
|
|
|
144
191
|
// Absent verdict = gateway could not vouch for this actor → fail-closed deny.
|
|
145
192
|
// A PRESENT verdict with no member (stranger) still flows through the
|
|
146
|
-
//
|
|
193
|
+
// stranger lane below; only a missing verdict short-circuits here.
|
|
147
194
|
const verdict = sourceMetadata?.trustVerdict;
|
|
148
195
|
if (verdict == null) {
|
|
149
196
|
log.info(
|
|
150
197
|
{ sourceChannel, externalUserId: canonicalSenderId },
|
|
151
198
|
"Ingress ACL: absent trust verdict, denying fail-closed",
|
|
152
199
|
);
|
|
153
|
-
return
|
|
154
|
-
resolvedMember: null,
|
|
155
|
-
earlyResponse: {
|
|
156
|
-
accepted: true,
|
|
157
|
-
denied: true,
|
|
158
|
-
reason: "not_a_member",
|
|
159
|
-
},
|
|
160
|
-
};
|
|
200
|
+
return failClosedDeny();
|
|
161
201
|
}
|
|
162
202
|
|
|
163
203
|
// Gateway attempted resolution but failed (DB error) → fail-closed deny,
|
|
@@ -168,18 +208,11 @@ export async function enforceIngressAcl(
|
|
|
168
208
|
{ sourceChannel, externalUserId: canonicalSenderId },
|
|
169
209
|
"Ingress ACL: gateway trust resolution failed, denying fail-closed",
|
|
170
210
|
);
|
|
171
|
-
return
|
|
172
|
-
resolvedMember: null,
|
|
173
|
-
earlyResponse: {
|
|
174
|
-
accepted: true,
|
|
175
|
-
denied: true,
|
|
176
|
-
reason: "not_a_member",
|
|
177
|
-
},
|
|
178
|
-
};
|
|
211
|
+
return failClosedDeny();
|
|
179
212
|
}
|
|
180
213
|
|
|
181
214
|
// Member resolved from the gateway verdict (ACL + identity only); null for a
|
|
182
|
-
// stranger verdict, which falls through to the non-member
|
|
215
|
+
// stranger verdict, which falls through to the non-member deny lane.
|
|
183
216
|
const resolvedMember: VerdictMember | null =
|
|
184
217
|
verdictMemberFromVerdict(verdict);
|
|
185
218
|
|
|
@@ -190,14 +223,92 @@ export async function enforceIngressAcl(
|
|
|
190
223
|
{ sourceChannel, externalUserId: canonicalSenderId },
|
|
191
224
|
"Ingress ACL: member verdict with unresolvable ACL, denying fail-closed",
|
|
192
225
|
);
|
|
193
|
-
return
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
return failClosedDeny();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// An unrecognized trust class is an unresolvable verdict (version skew,
|
|
230
|
+
// malformed payload), not a stranger. Fail safe: soft-deny with no
|
|
231
|
+
// stranger-lane side effects (no access-request card, no verification
|
|
232
|
+
// challenge, no canned reply) — never fail-stranger.
|
|
233
|
+
if (!isTrustClass(verdict.trustClass)) {
|
|
234
|
+
log.warn(
|
|
235
|
+
{
|
|
236
|
+
sourceChannel,
|
|
237
|
+
externalUserId: canonicalSenderId,
|
|
238
|
+
trustClass: verdict.trustClass,
|
|
199
239
|
},
|
|
200
|
-
|
|
240
|
+
"Ingress ACL: unrecognized trust class on verdict, denying fail-safe",
|
|
241
|
+
);
|
|
242
|
+
return failClosedDeny();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Guardian short-circuit ──
|
|
246
|
+
// A verdict classified `guardian` is admitted even when its same-channel
|
|
247
|
+
// member row is inactive (e.g. pending): the gateway classifies guardians
|
|
248
|
+
// by principal, so a guardian speaking on a channel where they hold no
|
|
249
|
+
// active guardian binding must not fall through the member-vs-stranger
|
|
250
|
+
// gates below — those would misroute the guardian into the stranger lane
|
|
251
|
+
// and fire an access request at the guardian themselves.
|
|
252
|
+
if (verdict.trustClass === "guardian") {
|
|
253
|
+
// The gateway proves guardian identity via a same-channel member row
|
|
254
|
+
// (the active binding address, or a row belonging to the guardian
|
|
255
|
+
// contact), so every guardian verdict carries a resolvable member row.
|
|
256
|
+
// A guardian verdict WITHOUT one is contradictory — cross-channel
|
|
257
|
+
// address collisions are not identity proofs and must never confer
|
|
258
|
+
// guardian capabilities. Fail safe: soft-deny with no stranger-lane
|
|
259
|
+
// side effects.
|
|
260
|
+
if (!resolvedMember) {
|
|
261
|
+
log.warn(
|
|
262
|
+
{ sourceChannel, externalUserId: canonicalSenderId },
|
|
263
|
+
"Ingress ACL: guardian verdict without a member row, denying fail-safe",
|
|
264
|
+
);
|
|
265
|
+
return failClosedDeny();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The gateway never classifies a blocked/revoked same-channel row as
|
|
269
|
+
// guardian (explicit per-channel governance wins over the principal
|
|
270
|
+
// check), so a verdict claiming both is contradictory. Fail safe:
|
|
271
|
+
// soft-deny with no stranger-lane side effects.
|
|
272
|
+
if (
|
|
273
|
+
resolvedMember.status === "blocked" ||
|
|
274
|
+
resolvedMember.status === "revoked"
|
|
275
|
+
) {
|
|
276
|
+
log.warn(
|
|
277
|
+
{
|
|
278
|
+
sourceChannel,
|
|
279
|
+
externalUserId: canonicalSenderId,
|
|
280
|
+
status: resolvedMember.status,
|
|
281
|
+
},
|
|
282
|
+
"Ingress ACL: contradictory guardian verdict with blocked/revoked member row, denying fail-safe",
|
|
283
|
+
);
|
|
284
|
+
return failClosedDeny();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// An explicit per-channel `policy: "deny"` on the guardian's own row is
|
|
288
|
+
// honored like blocked/revoked: explicit governance wins over
|
|
289
|
+
// classification. Deny with the accurate policy_deny reason but none of
|
|
290
|
+
// the stranger-lane side effects — the canned "ask the guardian" reply
|
|
291
|
+
// would be addressed at the guardian themselves.
|
|
292
|
+
if (resolvedMember.policy === "deny") {
|
|
293
|
+
log.info(
|
|
294
|
+
{ sourceChannel, externalUserId: canonicalSenderId },
|
|
295
|
+
"Ingress ACL: guardian member row carries policy deny, denying",
|
|
296
|
+
);
|
|
297
|
+
return {
|
|
298
|
+
resolvedMember,
|
|
299
|
+
earlyResponse: {
|
|
300
|
+
accepted: true,
|
|
301
|
+
denied: true,
|
|
302
|
+
reason: "policy_deny",
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
log.info(
|
|
308
|
+
{ sourceChannel, externalUserId: canonicalSenderId },
|
|
309
|
+
"Ingress ACL: guardian admitted via trust verdict",
|
|
310
|
+
);
|
|
311
|
+
return { resolvedMember };
|
|
201
312
|
}
|
|
202
313
|
|
|
203
314
|
// /start gv_<token> bootstrap commands must also bypass ACL — the user
|
|
@@ -207,12 +318,6 @@ export async function enforceIngressAcl(
|
|
|
207
318
|
commandIntentForAcl?.type === "start" &&
|
|
208
319
|
typeof commandIntentForAcl.payload === "string" &&
|
|
209
320
|
commandIntentForAcl.payload.startsWith("gv_");
|
|
210
|
-
const inviteAdapter = getInviteAdapterRegistry().get(sourceChannel);
|
|
211
|
-
const inviteToken = inviteAdapter?.extractInboundToken?.({
|
|
212
|
-
commandIntent: commandIntentForAcl,
|
|
213
|
-
content: trimmedContent,
|
|
214
|
-
sourceMetadata,
|
|
215
|
-
});
|
|
216
321
|
|
|
217
322
|
if (canonicalSenderId || hasSenderIdentityClaim) {
|
|
218
323
|
if (!resolvedMember) {
|
|
@@ -243,58 +348,6 @@ export async function enforceIngressAcl(
|
|
|
243
348
|
}
|
|
244
349
|
}
|
|
245
350
|
|
|
246
|
-
// ── Invite token intercept (non-member) ──
|
|
247
|
-
// /start invite deep links grant access without guardian approval.
|
|
248
|
-
// Runs BEFORE the policy-aware bypass so a valid /start iv_<token>
|
|
249
|
-
// always redeems and creates a member record — even when the
|
|
250
|
-
// admission policy is `strangers` (which would otherwise admit the
|
|
251
|
-
// sender as a non-member before the token is consumed).
|
|
252
|
-
if (inviteToken && denyNonMember) {
|
|
253
|
-
const inviteResult = await handleInviteTokenIntercept({
|
|
254
|
-
rawToken: inviteToken,
|
|
255
|
-
sourceChannel,
|
|
256
|
-
externalChatId: conversationExternalId,
|
|
257
|
-
externalMessageId,
|
|
258
|
-
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
259
|
-
senderName: actorDisplayName,
|
|
260
|
-
senderUsername: actorUsername,
|
|
261
|
-
replyCallbackUrl,
|
|
262
|
-
assistantId,
|
|
263
|
-
canonicalAssistantId,
|
|
264
|
-
});
|
|
265
|
-
if (inviteResult)
|
|
266
|
-
return {
|
|
267
|
-
resolvedMember: null,
|
|
268
|
-
earlyResponse: inviteResult,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// ── 6-digit invite code intercept (non-member) ──
|
|
273
|
-
// On channels with codeRedemptionEnabled, a bare 6-digit message may be
|
|
274
|
-
// an invite code. Attempt redemption; on failure (no matching code) fall
|
|
275
|
-
// through to normal processing — the number may be a regular message.
|
|
276
|
-
// Runs before the policy-aware bypass for the same reason as the token
|
|
277
|
-
// intercept above.
|
|
278
|
-
if (denyNonMember && /^\d{6}$/.test(trimmedContent)) {
|
|
279
|
-
const codeInterceptResult = await handleInviteCodeIntercept({
|
|
280
|
-
code: trimmedContent,
|
|
281
|
-
sourceChannel,
|
|
282
|
-
externalChatId: conversationExternalId,
|
|
283
|
-
externalMessageId,
|
|
284
|
-
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
285
|
-
senderName: actorDisplayName,
|
|
286
|
-
senderUsername: actorUsername,
|
|
287
|
-
replyCallbackUrl,
|
|
288
|
-
assistantId,
|
|
289
|
-
canonicalAssistantId,
|
|
290
|
-
});
|
|
291
|
-
if (codeInterceptResult)
|
|
292
|
-
return {
|
|
293
|
-
resolvedMember: null,
|
|
294
|
-
earlyResponse: codeInterceptResult,
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
351
|
// ── Policy-aware non-member bypass ──
|
|
299
352
|
// Skip the ACL deny gate so the admission floor stage emits the final
|
|
300
353
|
// verdict instead of the ACL prematurely firing guardian notifications,
|
|
@@ -308,7 +361,6 @@ export async function enforceIngressAcl(
|
|
|
308
361
|
// stranger there still can't reach the floor, but suppressing its
|
|
309
362
|
// self-verify challenge is a default-onboarding behavior change left
|
|
310
363
|
// for a separate §8.2 decision.
|
|
311
|
-
// Runs AFTER invite intercepts so valid tokens redeem first.
|
|
312
364
|
if (
|
|
313
365
|
denyNonMember &&
|
|
314
366
|
(effectiveAdmissionPolicy === "strangers" ||
|
|
@@ -323,10 +375,32 @@ export async function enforceIngressAcl(
|
|
|
323
375
|
"Ingress ACL: no member record, denying",
|
|
324
376
|
);
|
|
325
377
|
|
|
378
|
+
// Terminal deny: if the guardian already rejected this sender, skip all
|
|
379
|
+
// re-engagement (self-verify challenge + guardian notify) and deliver
|
|
380
|
+
// only the canned reply. Otherwise a denied sender whose first
|
|
381
|
+
// verification session expired would be handed a fresh, unusable
|
|
382
|
+
// challenge the guardian was never told about.
|
|
383
|
+
const nonMemberSenderId = canonicalSenderId ?? rawSenderId;
|
|
384
|
+
const terminallyDenied =
|
|
385
|
+
!!nonMemberSenderId &&
|
|
386
|
+
isAccessRequestDenied({
|
|
387
|
+
canonicalAssistantId,
|
|
388
|
+
sourceChannel,
|
|
389
|
+
actorExternalId: nonMemberSenderId,
|
|
390
|
+
});
|
|
391
|
+
|
|
326
392
|
// Slack-specific: send a verification challenge directly to the
|
|
327
393
|
// user's DM instead of requiring guardian-mediated approval. The
|
|
328
|
-
// user can reply with the code in the DM to self-verify.
|
|
329
|
-
|
|
394
|
+
// user can reply with the code in the DM to self-verify. Bots are
|
|
395
|
+
// excluded — a bot cannot return a code, so it goes straight to the
|
|
396
|
+
// guardian-notify lane and its introduction card offers direct trust.
|
|
397
|
+
if (
|
|
398
|
+
sourceChannel === "slack" &&
|
|
399
|
+
isBot !== true &&
|
|
400
|
+
(canonicalSenderId ?? rawSenderId) &&
|
|
401
|
+
!terminallyDenied &&
|
|
402
|
+
!isCallbackInteraction
|
|
403
|
+
) {
|
|
330
404
|
const slackVerifyResult = initiateSlackVerificationChallenge({
|
|
331
405
|
sourceChannel,
|
|
332
406
|
senderUserId: (canonicalSenderId ?? rawSenderId)!,
|
|
@@ -346,6 +420,7 @@ export async function enforceIngressAcl(
|
|
|
346
420
|
trimmedContent,
|
|
347
421
|
MESSAGE_PREVIEW_MAX_LENGTH,
|
|
348
422
|
),
|
|
423
|
+
isBot,
|
|
349
424
|
isStranger,
|
|
350
425
|
isRestricted,
|
|
351
426
|
messageTs,
|
|
@@ -402,7 +477,12 @@ export async function enforceIngressAcl(
|
|
|
402
477
|
// pipeline. Unlike Slack, we cannot DM the requester directly — the
|
|
403
478
|
// verification code is delivered to the guardian, who decides whether
|
|
404
479
|
// to share it with the email sender out-of-band.
|
|
405
|
-
if (
|
|
480
|
+
if (
|
|
481
|
+
sourceChannel === "email" &&
|
|
482
|
+
(canonicalSenderId ?? rawSenderId) &&
|
|
483
|
+
!terminallyDenied &&
|
|
484
|
+
!isCallbackInteraction
|
|
485
|
+
) {
|
|
406
486
|
const emailVerifyResult = initiateEmailVerificationChallenge({
|
|
407
487
|
sourceChannel,
|
|
408
488
|
senderUserId: (canonicalSenderId ?? rawSenderId)!,
|
|
@@ -421,6 +501,7 @@ export async function enforceIngressAcl(
|
|
|
421
501
|
trimmedContent,
|
|
422
502
|
MESSAGE_PREVIEW_MAX_LENGTH,
|
|
423
503
|
),
|
|
504
|
+
isBot,
|
|
424
505
|
isStranger,
|
|
425
506
|
isRestricted,
|
|
426
507
|
messageTs,
|
|
@@ -447,34 +528,52 @@ export async function enforceIngressAcl(
|
|
|
447
528
|
// Notify the guardian about the access request so they can approve/deny.
|
|
448
529
|
// Uses the shared helper which handles guardian binding lookup,
|
|
449
530
|
// deduplication, canonical request creation, and notification emission.
|
|
531
|
+
// Skipped for callback interactions — a button press must not create
|
|
532
|
+
// an access request — but the handshake window is still probed so the
|
|
533
|
+
// reply doesn't tell a just-approved sender they lack access.
|
|
450
534
|
let guardianNotified = false;
|
|
451
|
-
|
|
452
|
-
|
|
535
|
+
let handshakeInProgress = false;
|
|
536
|
+
if (isCallbackInteraction) {
|
|
537
|
+
handshakeInProgress = isApprovalHandshakeInProgress({
|
|
453
538
|
canonicalAssistantId,
|
|
454
539
|
sourceChannel,
|
|
455
|
-
|
|
456
|
-
actorExternalId: canonicalSenderId ?? rawSenderId,
|
|
457
|
-
actorDisplayName,
|
|
458
|
-
actorUsername,
|
|
459
|
-
messagePreview: truncate(
|
|
460
|
-
trimmedContent,
|
|
461
|
-
MESSAGE_PREVIEW_MAX_LENGTH,
|
|
462
|
-
),
|
|
463
|
-
isStranger,
|
|
464
|
-
isRestricted,
|
|
465
|
-
messageTs,
|
|
540
|
+
actorExternalId: (canonicalSenderId ?? rawSenderId)!,
|
|
466
541
|
});
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
542
|
+
} else {
|
|
543
|
+
try {
|
|
544
|
+
const accessResult = await notifyGuardianOfAccessRequest({
|
|
545
|
+
canonicalAssistantId,
|
|
546
|
+
sourceChannel,
|
|
547
|
+
conversationExternalId,
|
|
548
|
+
actorExternalId: canonicalSenderId ?? rawSenderId,
|
|
549
|
+
actorDisplayName,
|
|
550
|
+
actorUsername,
|
|
551
|
+
messagePreview: truncate(
|
|
552
|
+
trimmedContent,
|
|
553
|
+
MESSAGE_PREVIEW_MAX_LENGTH,
|
|
554
|
+
),
|
|
555
|
+
isBot,
|
|
556
|
+
isStranger,
|
|
557
|
+
isRestricted,
|
|
558
|
+
messageTs,
|
|
559
|
+
});
|
|
560
|
+
guardianNotified = accessResult.notified;
|
|
561
|
+
handshakeInProgress =
|
|
562
|
+
!accessResult.notified &&
|
|
563
|
+
accessResult.reason === "approval_pending_verification";
|
|
564
|
+
} catch (err) {
|
|
565
|
+
log.error(
|
|
566
|
+
{ err, sourceChannel, conversationExternalId },
|
|
567
|
+
"Failed to notify guardian of access request",
|
|
568
|
+
);
|
|
569
|
+
}
|
|
473
570
|
}
|
|
474
571
|
|
|
475
|
-
const replyText =
|
|
476
|
-
|
|
477
|
-
|
|
572
|
+
const replyText = await composeAccessDenialReply({
|
|
573
|
+
sourceChannel,
|
|
574
|
+
guardianNotified,
|
|
575
|
+
handshakeInProgress,
|
|
576
|
+
});
|
|
478
577
|
let replyDelivered = false;
|
|
479
578
|
if (replyCallbackUrl) {
|
|
480
579
|
const replyPayload: Parameters<typeof deliverChannelReply>[1] = {
|
|
@@ -543,63 +642,6 @@ export async function enforceIngressAcl(
|
|
|
543
642
|
}
|
|
544
643
|
}
|
|
545
644
|
|
|
546
|
-
// ── Invite token intercept (inactive member) ──
|
|
547
|
-
// Invite tokens can reactivate revoked/pending members without
|
|
548
|
-
// requiring guardian approval, but blocked members are excluded so
|
|
549
|
-
// they are short-circuited at the ACL layer rather than entering the
|
|
550
|
-
// redemption path. Runs BEFORE the policy-aware bypass so a valid
|
|
551
|
-
// invite always redeems and reactivates the member record, rather
|
|
552
|
-
// than the bypass admitting the sender in their inactive state.
|
|
553
|
-
if (!isBlockedMember && inviteToken && denyInactiveMember) {
|
|
554
|
-
const inviteResult = await handleInviteTokenIntercept({
|
|
555
|
-
rawToken: inviteToken,
|
|
556
|
-
sourceChannel,
|
|
557
|
-
externalChatId: conversationExternalId,
|
|
558
|
-
externalMessageId,
|
|
559
|
-
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
560
|
-
senderName: actorDisplayName,
|
|
561
|
-
senderUsername: actorUsername,
|
|
562
|
-
replyCallbackUrl,
|
|
563
|
-
assistantId,
|
|
564
|
-
canonicalAssistantId,
|
|
565
|
-
});
|
|
566
|
-
if (inviteResult)
|
|
567
|
-
return {
|
|
568
|
-
resolvedMember: null,
|
|
569
|
-
earlyResponse: inviteResult,
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// ── 6-digit invite code intercept (inactive member) ──
|
|
574
|
-
// Codes can reactivate revoked/pending members; non-matching codes
|
|
575
|
-
// fall through. Blocked members are excluded here for consistency —
|
|
576
|
-
// the redemption service would reject them anyway, but early exit
|
|
577
|
-
// avoids unnecessary work. Runs before the policy-aware bypass for
|
|
578
|
-
// the same reason as the token intercept above.
|
|
579
|
-
if (
|
|
580
|
-
!isBlockedMember &&
|
|
581
|
-
denyInactiveMember &&
|
|
582
|
-
/^\d{6}$/.test(trimmedContent)
|
|
583
|
-
) {
|
|
584
|
-
const codeInterceptResult = await handleInviteCodeIntercept({
|
|
585
|
-
code: trimmedContent,
|
|
586
|
-
sourceChannel,
|
|
587
|
-
externalChatId: conversationExternalId,
|
|
588
|
-
externalMessageId,
|
|
589
|
-
senderExternalUserId: canonicalSenderId ?? rawSenderId,
|
|
590
|
-
senderName: actorDisplayName,
|
|
591
|
-
senderUsername: actorUsername,
|
|
592
|
-
replyCallbackUrl,
|
|
593
|
-
assistantId,
|
|
594
|
-
canonicalAssistantId,
|
|
595
|
-
});
|
|
596
|
-
if (codeInterceptResult)
|
|
597
|
-
return {
|
|
598
|
-
resolvedMember: null,
|
|
599
|
-
earlyResponse: codeInterceptResult,
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
645
|
// ── Policy-aware inactive-member bypass ──
|
|
604
646
|
// `strangers` (floor 1): admit non-blocked, non-revoked senders
|
|
605
647
|
// (pending/unverified bypass the inactive-member deny gate).
|
|
@@ -616,7 +658,12 @@ export async function enforceIngressAcl(
|
|
|
616
658
|
// there, verifying reaches `trusted_contact` (rank 3 ≥ floor 3), so
|
|
617
659
|
// the challenge legitimately upgrades the sender into access.
|
|
618
660
|
// In every case skip the deny gate so the admission stage decides.
|
|
619
|
-
//
|
|
661
|
+
//
|
|
662
|
+
// A guardian-denied sender is persisted as an unverified contact and is
|
|
663
|
+
// admitted here on the same rank-vs-floor terms as any other unverified
|
|
664
|
+
// contact (admitted under `any_contact`/`strangers`, denied under
|
|
665
|
+
// stricter floors). The deny suppresses re-prompting, not admission;
|
|
666
|
+
// holding a denied contact out of every floor is the block action's job.
|
|
620
667
|
if (!isBlockedMember && denyInactiveMember) {
|
|
621
668
|
if (
|
|
622
669
|
(effectiveAdmissionPolicy === "strangers" &&
|
|
@@ -640,13 +687,31 @@ export async function enforceIngressAcl(
|
|
|
640
687
|
"Ingress ACL: member not active, denying",
|
|
641
688
|
);
|
|
642
689
|
|
|
690
|
+
// Terminal deny: a sender the guardian already rejected must not be
|
|
691
|
+
// handed a fresh self-verify challenge on re-contact (the guardian is
|
|
692
|
+
// no longer notified, so the code would go nowhere). Skip the
|
|
693
|
+
// challenge and fall through to the canned reply.
|
|
694
|
+
const inactiveSenderId = canonicalSenderId ?? rawSenderId;
|
|
695
|
+
const terminallyDenied =
|
|
696
|
+
!isBlockedMember &&
|
|
697
|
+
!!inactiveSenderId &&
|
|
698
|
+
isAccessRequestDenied({
|
|
699
|
+
canonicalAssistantId,
|
|
700
|
+
sourceChannel,
|
|
701
|
+
actorExternalId: inactiveSenderId,
|
|
702
|
+
});
|
|
703
|
+
|
|
643
704
|
// Slack-specific: re-verify inactive members via DM challenge
|
|
644
705
|
// (same as non-member path). Blocked members are excluded —
|
|
645
|
-
// the guardian made an explicit decision to block them.
|
|
706
|
+
// the guardian made an explicit decision to block them. Bots are
|
|
707
|
+
// excluded — a bot cannot return a code.
|
|
646
708
|
if (
|
|
647
709
|
sourceChannel === "slack" &&
|
|
710
|
+
isBot !== true &&
|
|
648
711
|
resolvedMember.status !== "blocked" &&
|
|
649
|
-
(canonicalSenderId ?? rawSenderId)
|
|
712
|
+
(canonicalSenderId ?? rawSenderId) &&
|
|
713
|
+
!terminallyDenied &&
|
|
714
|
+
!isCallbackInteraction
|
|
650
715
|
) {
|
|
651
716
|
const slackVerifyResult = initiateSlackVerificationChallenge({
|
|
652
717
|
sourceChannel,
|
|
@@ -669,6 +734,7 @@ export async function enforceIngressAcl(
|
|
|
669
734
|
trimmedContent,
|
|
670
735
|
MESSAGE_PREVIEW_MAX_LENGTH,
|
|
671
736
|
),
|
|
737
|
+
isBot,
|
|
672
738
|
isStranger,
|
|
673
739
|
isRestricted,
|
|
674
740
|
messageTs,
|
|
@@ -720,39 +786,56 @@ export async function enforceIngressAcl(
|
|
|
720
786
|
// For revoked/pending members, notify the guardian so they can
|
|
721
787
|
// re-approve. Blocked members are intentionally excluded — the
|
|
722
788
|
// guardian already made an explicit decision to block them.
|
|
789
|
+
// Callback interactions never create an access request; the
|
|
790
|
+
// handshake window is still probed for reply copy.
|
|
723
791
|
let guardianNotified = false;
|
|
792
|
+
let handshakeInProgress = false;
|
|
724
793
|
if (resolvedMember.status !== "blocked") {
|
|
725
|
-
|
|
726
|
-
|
|
794
|
+
if (isCallbackInteraction) {
|
|
795
|
+
handshakeInProgress = isApprovalHandshakeInProgress({
|
|
727
796
|
canonicalAssistantId,
|
|
728
797
|
sourceChannel,
|
|
729
|
-
|
|
730
|
-
actorExternalId: canonicalSenderId ?? rawSenderId,
|
|
731
|
-
actorDisplayName,
|
|
732
|
-
actorUsername,
|
|
733
|
-
previousMemberStatus: channelStatusToMemberStatus(
|
|
734
|
-
resolvedMember.status,
|
|
735
|
-
),
|
|
736
|
-
messagePreview: truncate(
|
|
737
|
-
trimmedContent,
|
|
738
|
-
MESSAGE_PREVIEW_MAX_LENGTH,
|
|
739
|
-
),
|
|
740
|
-
isStranger,
|
|
741
|
-
isRestricted,
|
|
742
|
-
messageTs,
|
|
798
|
+
actorExternalId: (canonicalSenderId ?? rawSenderId)!,
|
|
743
799
|
});
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
800
|
+
} else {
|
|
801
|
+
try {
|
|
802
|
+
const accessResult = await notifyGuardianOfAccessRequest({
|
|
803
|
+
canonicalAssistantId,
|
|
804
|
+
sourceChannel,
|
|
805
|
+
conversationExternalId,
|
|
806
|
+
actorExternalId: canonicalSenderId ?? rawSenderId,
|
|
807
|
+
actorDisplayName,
|
|
808
|
+
actorUsername,
|
|
809
|
+
previousMemberStatus: channelStatusToMemberStatus(
|
|
810
|
+
resolvedMember.status,
|
|
811
|
+
),
|
|
812
|
+
messagePreview: truncate(
|
|
813
|
+
trimmedContent,
|
|
814
|
+
MESSAGE_PREVIEW_MAX_LENGTH,
|
|
815
|
+
),
|
|
816
|
+
isBot,
|
|
817
|
+
isStranger,
|
|
818
|
+
isRestricted,
|
|
819
|
+
messageTs,
|
|
820
|
+
});
|
|
821
|
+
guardianNotified = accessResult.notified;
|
|
822
|
+
handshakeInProgress =
|
|
823
|
+
!accessResult.notified &&
|
|
824
|
+
accessResult.reason === "approval_pending_verification";
|
|
825
|
+
} catch (err) {
|
|
826
|
+
log.error(
|
|
827
|
+
{ err, sourceChannel, conversationExternalId },
|
|
828
|
+
"Failed to notify guardian of access request",
|
|
829
|
+
);
|
|
830
|
+
}
|
|
750
831
|
}
|
|
751
832
|
}
|
|
752
833
|
|
|
753
|
-
const inactiveReplyText =
|
|
754
|
-
|
|
755
|
-
|
|
834
|
+
const inactiveReplyText = await composeAccessDenialReply({
|
|
835
|
+
sourceChannel,
|
|
836
|
+
guardianNotified,
|
|
837
|
+
handshakeInProgress,
|
|
838
|
+
});
|
|
756
839
|
let inactiveReplyDelivered = false;
|
|
757
840
|
if (replyCallbackUrl) {
|
|
758
841
|
const inactiveReplyPayload: Parameters<
|
|
@@ -839,359 +922,6 @@ export async function enforceIngressAcl(
|
|
|
839
922
|
};
|
|
840
923
|
}
|
|
841
924
|
|
|
842
|
-
// ---------------------------------------------------------------------------
|
|
843
|
-
// Invite token intercept
|
|
844
|
-
// ---------------------------------------------------------------------------
|
|
845
|
-
|
|
846
|
-
/**
|
|
847
|
-
* Handle an inbound invite token for a non-member or inactive member.
|
|
848
|
-
*
|
|
849
|
-
* Redeems the invite, delivers a deterministic reply, and returns a Response
|
|
850
|
-
* to short-circuit the handler. Returns `null` when the intercept should not
|
|
851
|
-
* fire (e.g. already_member outcome — let normal flow handle it).
|
|
852
|
-
*/
|
|
853
|
-
async function handleInviteTokenIntercept(params: {
|
|
854
|
-
rawToken: string;
|
|
855
|
-
sourceChannel: ChannelId;
|
|
856
|
-
externalChatId: string;
|
|
857
|
-
externalMessageId: string;
|
|
858
|
-
senderExternalUserId?: string;
|
|
859
|
-
senderName?: string;
|
|
860
|
-
senderUsername?: string;
|
|
861
|
-
replyCallbackUrl?: string;
|
|
862
|
-
assistantId?: string;
|
|
863
|
-
canonicalAssistantId: string;
|
|
864
|
-
}): Promise<Record<string, unknown> | null> {
|
|
865
|
-
const {
|
|
866
|
-
rawToken,
|
|
867
|
-
sourceChannel,
|
|
868
|
-
externalChatId,
|
|
869
|
-
externalMessageId,
|
|
870
|
-
senderExternalUserId,
|
|
871
|
-
senderName,
|
|
872
|
-
senderUsername,
|
|
873
|
-
replyCallbackUrl,
|
|
874
|
-
assistantId,
|
|
875
|
-
canonicalAssistantId,
|
|
876
|
-
} = params;
|
|
877
|
-
|
|
878
|
-
// Record the inbound event for dedup tracking BEFORE performing redemption.
|
|
879
|
-
// Without this, duplicate webhook deliveries (common with Telegram) would
|
|
880
|
-
// not be tracked: the first delivery redeems the invite and returns early,
|
|
881
|
-
// then the retry finds an active member, passes ACL, and the raw
|
|
882
|
-
// /start iv_<token> message leaks into the agent pipeline.
|
|
883
|
-
const dedupResult = recordInbound(
|
|
884
|
-
sourceChannel,
|
|
885
|
-
externalChatId,
|
|
886
|
-
externalMessageId,
|
|
887
|
-
{ assistantId: canonicalAssistantId },
|
|
888
|
-
);
|
|
889
|
-
|
|
890
|
-
if (dedupResult.duplicate) {
|
|
891
|
-
return {
|
|
892
|
-
accepted: true,
|
|
893
|
-
duplicate: true,
|
|
894
|
-
eventId: dedupResult.eventId,
|
|
895
|
-
};
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
const outcome = await redeemInvite({
|
|
899
|
-
rawToken,
|
|
900
|
-
sourceChannel,
|
|
901
|
-
externalUserId: senderExternalUserId,
|
|
902
|
-
externalChatId,
|
|
903
|
-
displayName: senderName,
|
|
904
|
-
username: senderUsername,
|
|
905
|
-
assistantId: canonicalAssistantId,
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
log.info(
|
|
909
|
-
{
|
|
910
|
-
sourceChannel,
|
|
911
|
-
externalChatId: params.externalChatId,
|
|
912
|
-
ok: outcome.ok,
|
|
913
|
-
type: outcome.ok ? outcome.type : undefined,
|
|
914
|
-
reason: !outcome.ok ? outcome.reason : undefined,
|
|
915
|
-
},
|
|
916
|
-
"Invite token intercept: redemption result",
|
|
917
|
-
);
|
|
918
|
-
|
|
919
|
-
// already_member means the user has an active record — let the normal
|
|
920
|
-
// flow handle them (they passed ACL or the member is active).
|
|
921
|
-
if (outcome.ok && outcome.type === "already_member") {
|
|
922
|
-
// Deliver a quick acknowledgement and short-circuit so the user
|
|
923
|
-
// does not trigger the deny gate or a duplicate agent loop.
|
|
924
|
-
const replyText = getInviteRedemptionReply(outcome);
|
|
925
|
-
if (replyCallbackUrl) {
|
|
926
|
-
try {
|
|
927
|
-
await deliverChannelReply(replyCallbackUrl, {
|
|
928
|
-
chatId: externalChatId,
|
|
929
|
-
text: replyText,
|
|
930
|
-
assistantId,
|
|
931
|
-
});
|
|
932
|
-
} catch (err) {
|
|
933
|
-
log.error(
|
|
934
|
-
{ err, externalChatId },
|
|
935
|
-
"Failed to deliver invite already-member reply",
|
|
936
|
-
);
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
markProcessed(dedupResult.eventId);
|
|
940
|
-
return {
|
|
941
|
-
accepted: true,
|
|
942
|
-
eventId: dedupResult.eventId,
|
|
943
|
-
inviteRedemption: "already_member",
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
const replyText = getInviteRedemptionReply(outcome);
|
|
948
|
-
|
|
949
|
-
if (replyCallbackUrl) {
|
|
950
|
-
try {
|
|
951
|
-
await deliverChannelReply(replyCallbackUrl, {
|
|
952
|
-
chatId: externalChatId,
|
|
953
|
-
text: replyText,
|
|
954
|
-
assistantId,
|
|
955
|
-
});
|
|
956
|
-
} catch (err) {
|
|
957
|
-
log.error(
|
|
958
|
-
{ err, externalChatId },
|
|
959
|
-
"Failed to deliver invite redemption reply",
|
|
960
|
-
);
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
if (outcome.ok && outcome.type === "redeemed") {
|
|
965
|
-
markProcessed(dedupResult.eventId);
|
|
966
|
-
return {
|
|
967
|
-
accepted: true,
|
|
968
|
-
eventId: dedupResult.eventId,
|
|
969
|
-
inviteRedemption: "redeemed",
|
|
970
|
-
memberId: outcome.memberId,
|
|
971
|
-
};
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
// Failed redemption — inform the user and deny
|
|
975
|
-
markProcessed(dedupResult.eventId);
|
|
976
|
-
return {
|
|
977
|
-
accepted: true,
|
|
978
|
-
eventId: dedupResult.eventId,
|
|
979
|
-
denied: true,
|
|
980
|
-
inviteRedemption: outcome.reason,
|
|
981
|
-
};
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
// ---------------------------------------------------------------------------
|
|
985
|
-
// 6-digit invite code intercept
|
|
986
|
-
// ---------------------------------------------------------------------------
|
|
987
|
-
|
|
988
|
-
/**
|
|
989
|
-
* Handle a bare 6-digit message as a potential invite code redemption.
|
|
990
|
-
*
|
|
991
|
-
* Checks channel policy (codeRedemptionEnabled), attempts redemption via
|
|
992
|
-
* `redeemInviteByCode`, and returns a Response to short-circuit the handler
|
|
993
|
-
* on success. Returns `null` when the code does not match any active invite,
|
|
994
|
-
* allowing the message to fall through to normal processing.
|
|
995
|
-
*/
|
|
996
|
-
async function handleInviteCodeIntercept(params: {
|
|
997
|
-
code: string;
|
|
998
|
-
sourceChannel: ChannelId;
|
|
999
|
-
externalChatId: string;
|
|
1000
|
-
externalMessageId: string;
|
|
1001
|
-
senderExternalUserId?: string;
|
|
1002
|
-
senderName?: string;
|
|
1003
|
-
senderUsername?: string;
|
|
1004
|
-
replyCallbackUrl?: string;
|
|
1005
|
-
assistantId?: string;
|
|
1006
|
-
canonicalAssistantId: string;
|
|
1007
|
-
}): Promise<Record<string, unknown> | null> {
|
|
1008
|
-
const {
|
|
1009
|
-
code,
|
|
1010
|
-
sourceChannel,
|
|
1011
|
-
externalChatId,
|
|
1012
|
-
externalMessageId,
|
|
1013
|
-
senderExternalUserId,
|
|
1014
|
-
senderName,
|
|
1015
|
-
senderUsername,
|
|
1016
|
-
replyCallbackUrl,
|
|
1017
|
-
assistantId,
|
|
1018
|
-
canonicalAssistantId,
|
|
1019
|
-
} = params;
|
|
1020
|
-
|
|
1021
|
-
// Skip channels that don't support code redemption
|
|
1022
|
-
if (!isInviteCodeRedemptionEnabled(sourceChannel)) {
|
|
1023
|
-
return null;
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// Pre-check: verify a matching invite exists before committing to handle
|
|
1027
|
-
// this message. A bare 6-digit number may be a regular message, so we
|
|
1028
|
-
// must not record inbound dedup until we know the code maps to an invite.
|
|
1029
|
-
const codeHash = hashVoiceCode(code);
|
|
1030
|
-
const candidateInvite = findByInviteCodeHash(codeHash, sourceChannel);
|
|
1031
|
-
if (!candidateInvite) {
|
|
1032
|
-
// The code doesn't match any invite on this channel. Before falling
|
|
1033
|
-
// through to normal processing, check if it matches on a different
|
|
1034
|
-
// channel — if so, inform the user instead of silently ignoring it.
|
|
1035
|
-
const crossChannelInvite = findByInviteCodeHashAnyChannel(codeHash);
|
|
1036
|
-
if (crossChannelInvite) {
|
|
1037
|
-
// Record inbound for dedup tracking — without this, duplicate webhook
|
|
1038
|
-
// deliveries would re-enter ACL and send the mismatch reply again.
|
|
1039
|
-
const dedupResult = recordInbound(
|
|
1040
|
-
sourceChannel,
|
|
1041
|
-
externalChatId,
|
|
1042
|
-
externalMessageId,
|
|
1043
|
-
{ assistantId: canonicalAssistantId },
|
|
1044
|
-
);
|
|
1045
|
-
|
|
1046
|
-
if (dedupResult.duplicate) {
|
|
1047
|
-
return {
|
|
1048
|
-
accepted: true,
|
|
1049
|
-
duplicate: true,
|
|
1050
|
-
eventId: dedupResult.eventId,
|
|
1051
|
-
};
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
const mismatchReply = "This invite is not valid for this channel.";
|
|
1055
|
-
if (replyCallbackUrl) {
|
|
1056
|
-
try {
|
|
1057
|
-
await deliverChannelReply(replyCallbackUrl, {
|
|
1058
|
-
chatId: externalChatId,
|
|
1059
|
-
text: mismatchReply,
|
|
1060
|
-
assistantId,
|
|
1061
|
-
});
|
|
1062
|
-
} catch (err) {
|
|
1063
|
-
log.error(
|
|
1064
|
-
{ err, externalChatId },
|
|
1065
|
-
"Failed to deliver invite code channel-mismatch reply",
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
markProcessed(dedupResult.eventId);
|
|
1070
|
-
return {
|
|
1071
|
-
accepted: true,
|
|
1072
|
-
eventId: dedupResult.eventId,
|
|
1073
|
-
denied: true,
|
|
1074
|
-
inviteRedemption: "channel_mismatch",
|
|
1075
|
-
};
|
|
1076
|
-
}
|
|
1077
|
-
return null;
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
// Record the inbound event for dedup tracking BEFORE performing redemption,
|
|
1081
|
-
// matching the token intercept path. Without this, duplicate webhook
|
|
1082
|
-
// deliveries could slip through: the first delivery redeems the invite and
|
|
1083
|
-
// activates membership, then a retry finds an active member, passes ACL,
|
|
1084
|
-
// and the raw 6-digit message leaks into the agent pipeline.
|
|
1085
|
-
const dedupResult = recordInbound(
|
|
1086
|
-
sourceChannel,
|
|
1087
|
-
externalChatId,
|
|
1088
|
-
externalMessageId,
|
|
1089
|
-
{ assistantId: canonicalAssistantId },
|
|
1090
|
-
);
|
|
1091
|
-
|
|
1092
|
-
if (dedupResult.duplicate) {
|
|
1093
|
-
return {
|
|
1094
|
-
accepted: true,
|
|
1095
|
-
duplicate: true,
|
|
1096
|
-
eventId: dedupResult.eventId,
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
let outcome: Awaited<ReturnType<typeof redeemInviteByCode>>;
|
|
1101
|
-
try {
|
|
1102
|
-
outcome = await redeemInviteByCode({
|
|
1103
|
-
code,
|
|
1104
|
-
sourceChannel,
|
|
1105
|
-
externalUserId: senderExternalUserId,
|
|
1106
|
-
externalChatId,
|
|
1107
|
-
displayName: senderName,
|
|
1108
|
-
username: senderUsername,
|
|
1109
|
-
assistantId: canonicalAssistantId,
|
|
1110
|
-
});
|
|
1111
|
-
} catch (err) {
|
|
1112
|
-
// Redemption threw — roll back the dedup record so webhook retries
|
|
1113
|
-
// can re-attempt instead of short-circuiting as duplicates.
|
|
1114
|
-
log.error(
|
|
1115
|
-
{ err, sourceChannel, externalChatId },
|
|
1116
|
-
"Invite code intercept: redemption threw, rolling back dedup record",
|
|
1117
|
-
);
|
|
1118
|
-
deleteInbound(dedupResult.eventId);
|
|
1119
|
-
throw err;
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
|
-
log.info(
|
|
1123
|
-
{
|
|
1124
|
-
sourceChannel,
|
|
1125
|
-
externalChatId,
|
|
1126
|
-
ok: outcome.ok,
|
|
1127
|
-
type: outcome.ok ? outcome.type : undefined,
|
|
1128
|
-
reason: !outcome.ok ? outcome.reason : undefined,
|
|
1129
|
-
},
|
|
1130
|
-
"Invite code intercept: redemption result",
|
|
1131
|
-
);
|
|
1132
|
-
|
|
1133
|
-
// already_member: deliver acknowledgement and short-circuit
|
|
1134
|
-
if (outcome.ok && outcome.type === "already_member") {
|
|
1135
|
-
const replyText = getInviteRedemptionReply(outcome);
|
|
1136
|
-
if (replyCallbackUrl) {
|
|
1137
|
-
try {
|
|
1138
|
-
await deliverChannelReply(replyCallbackUrl, {
|
|
1139
|
-
chatId: externalChatId,
|
|
1140
|
-
text: replyText,
|
|
1141
|
-
assistantId,
|
|
1142
|
-
});
|
|
1143
|
-
} catch (err) {
|
|
1144
|
-
log.error(
|
|
1145
|
-
{ err, externalChatId },
|
|
1146
|
-
"Failed to deliver invite code already-member reply",
|
|
1147
|
-
);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
markProcessed(dedupResult.eventId);
|
|
1151
|
-
return {
|
|
1152
|
-
accepted: true,
|
|
1153
|
-
eventId: dedupResult.eventId,
|
|
1154
|
-
inviteRedemption: "already_member",
|
|
1155
|
-
};
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
const replyText = getInviteRedemptionReply(outcome);
|
|
1159
|
-
|
|
1160
|
-
if (replyCallbackUrl) {
|
|
1161
|
-
try {
|
|
1162
|
-
await deliverChannelReply(replyCallbackUrl, {
|
|
1163
|
-
chatId: externalChatId,
|
|
1164
|
-
text: replyText,
|
|
1165
|
-
assistantId,
|
|
1166
|
-
});
|
|
1167
|
-
} catch (err) {
|
|
1168
|
-
log.error(
|
|
1169
|
-
{ err, externalChatId },
|
|
1170
|
-
"Failed to deliver invite code redemption reply",
|
|
1171
|
-
);
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
if (outcome.ok && outcome.type === "redeemed") {
|
|
1176
|
-
markProcessed(dedupResult.eventId);
|
|
1177
|
-
return {
|
|
1178
|
-
accepted: true,
|
|
1179
|
-
eventId: dedupResult.eventId,
|
|
1180
|
-
inviteRedemption: "redeemed",
|
|
1181
|
-
memberId: outcome.memberId,
|
|
1182
|
-
};
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
// Failed redemption (expired, revoked, etc.) — inform and deny
|
|
1186
|
-
markProcessed(dedupResult.eventId);
|
|
1187
|
-
return {
|
|
1188
|
-
accepted: true,
|
|
1189
|
-
eventId: dedupResult.eventId,
|
|
1190
|
-
denied: true,
|
|
1191
|
-
inviteRedemption: !outcome.ok ? outcome.reason : undefined,
|
|
1192
|
-
};
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
925
|
// ---------------------------------------------------------------------------
|
|
1196
926
|
// Channel verification challenges
|
|
1197
927
|
// ---------------------------------------------------------------------------
|