@vellumai/assistant 0.11.7 → 0.11.8-staging.1
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 +10 -10
- package/Dockerfile +1 -0
- package/docs/architecture/security.md +1 -1
- package/docs/error-handling.md +1 -2
- package/docs/guardian-request-flow.md +51 -5
- package/docs/trusted-contact-access.md +13 -12
- package/knip.json +1 -0
- package/node_modules/@vellumai/avatar-catalog/package.json +19 -0
- package/{src/avatar/character-components.ts → node_modules/@vellumai/avatar-catalog/src/catalog.ts} +12 -44
- package/node_modules/@vellumai/avatar-catalog/src/colors.ts +19 -0
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +25 -0
- package/node_modules/@vellumai/avatar-catalog/src/types.ts +47 -0
- package/node_modules/@vellumai/avatar-catalog/tsconfig.json +20 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +43 -12
- package/node_modules/@vellumai/environments/package.json +2 -1
- package/node_modules/@vellumai/environments/src/__tests__/test-path-guard.test.ts +89 -0
- package/node_modules/@vellumai/environments/src/test-path-guard.ts +113 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +8 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/invite-contract.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +46 -0
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +62 -21
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +14 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +53 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +11 -5
- package/node_modules/@vellumai/gateway-client/src/invite-contract.ts +1 -0
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/channels.ts +7 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/no-response.ts +84 -0
- package/openapi.yaml +481 -9
- package/package.json +3 -1
- package/scripts/sync-llm-catalog.ts +3 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +16 -0
- package/src/__tests__/agent-loop-compacted-history-transform.test.ts +304 -0
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +3 -0
- package/src/__tests__/agent-wake-override-profile.test.ts +3 -0
- package/src/__tests__/anthropic-provider.test.ts +56 -0
- package/src/__tests__/approval-cascade.test.ts +0 -13
- package/src/__tests__/attachments-store.test.ts +117 -6
- package/src/__tests__/attachments.test.ts +122 -0
- package/src/__tests__/bundled-asset.test.ts +22 -1
- package/src/__tests__/channel-approval.test.ts +14 -7
- package/src/__tests__/channel-approvals.test.ts +3 -3
- package/src/__tests__/channel-delivery-store.test.ts +58 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +1 -0
- package/src/__tests__/channel-invite-transport.test.ts +1 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +3 -3
- package/src/__tests__/compaction-events.test.ts +0 -12
- package/src/__tests__/compactor-call-site-logging.test.ts +7 -1
- package/src/__tests__/compactor-retained-image-validation.test.ts +15 -0
- package/src/__tests__/config-schema.test.ts +35 -0
- package/src/__tests__/contacts-write.test.ts +141 -3
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +0 -12
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -13
- package/src/__tests__/conversation-agent-loop.test.ts +95 -13
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -13
- package/src/__tests__/conversation-load-history-repair.test.ts +418 -0
- package/src/__tests__/conversation-media-retry.test.ts +33 -2
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +0 -12
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -13
- package/src/__tests__/conversation-queue.test.ts +4 -13
- package/src/__tests__/conversation-retry-route.test.ts +5 -0
- package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
- package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
- package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
- package/src/__tests__/conversation-sight-frames.test.ts +615 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -13
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -13
- package/src/__tests__/conversation-speed-override.test.ts +0 -12
- package/src/__tests__/conversation-store-ephemeral.test.ts +0 -12
- package/src/__tests__/conversation-summarize-route.test.ts +5 -0
- package/src/__tests__/conversation-summarize-up-to.test.ts +102 -0
- package/src/__tests__/conversation-wait-for-idle.test.ts +0 -12
- package/src/__tests__/conversation-workspace-cache-state.test.ts +0 -5
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -17
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -17
- package/src/__tests__/credential-health-service.test.ts +45 -0
- package/src/__tests__/credential-record-retire.test.ts +256 -0
- package/src/__tests__/credential-record-write-through.test.ts +8 -4
- package/src/__tests__/db-conversation-fork-lineage-migration.test.ts +0 -4
- package/src/__tests__/db-conversation-inference-profile-migration.test.ts +0 -4
- package/src/__tests__/db-llm-request-log-provider-migration.test.ts +0 -8
- package/src/__tests__/delete-propagation.test.ts +121 -3
- package/src/__tests__/evict-conversations-for-reload.test.ts +46 -6
- package/src/__tests__/file-ops-service.test.ts +8 -3
- package/src/__tests__/gemini-provider.test.ts +64 -0
- package/src/__tests__/guardian-action-service-scope.test.ts +118 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +299 -5
- package/src/__tests__/guardian-expiry-notifier.test.ts +117 -13
- package/src/__tests__/guardian-gateway-sim.test.ts +119 -0
- package/src/__tests__/guardian-gateway-sim.ts +56 -20
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +6 -0
- package/src/__tests__/guardian-request-status-sync.test.ts +162 -0
- package/src/__tests__/guardian-wait-controller.test.ts +6 -0
- package/src/__tests__/headless-browser-mode.test.ts +27 -0
- package/src/__tests__/helpers/mock-conversation.ts +2 -0
- package/src/__tests__/list-messages-system-card.test.ts +70 -0
- package/src/__tests__/llm-catalog-parity.test.ts +2 -0
- package/src/__tests__/managed-profile-guard.test.ts +2 -2
- package/src/__tests__/media-resolve-image-validation.test.ts +106 -0
- package/src/__tests__/media-stream-server-integration.test.ts +6 -0
- package/src/__tests__/non-member-access-request.test.ts +12 -0
- package/src/__tests__/notification-discord-adapter.test.ts +76 -4
- package/src/__tests__/oauth-commands-routes.test.ts +21 -0
- package/src/__tests__/openai-provider.test.ts +63 -0
- package/src/__tests__/outbound-slack-persistence.test.ts +114 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +63 -1
- package/src/__tests__/persist-unsendable-image.test.ts +19 -0
- package/src/__tests__/plugin-app-serve-routes.test.ts +2 -2
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/process-message-background-slack.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +1 -1
- package/src/__tests__/processing-flag-persist-failure.test.ts +0 -5
- package/src/__tests__/prompt-cache-cross-turn-stability.test.ts +0 -17
- package/src/__tests__/qdrant-manager.test.ts +6 -0
- package/src/__tests__/reaction-persistence.test.ts +58 -62
- package/src/__tests__/retry-thinking-adaptive-only.test.ts +67 -0
- package/src/__tests__/server-history-render.test.ts +22 -0
- package/src/__tests__/sight-frame-retention.test.ts +362 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +44 -24
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +29 -1
- package/src/__tests__/subagent-fork-notifications.test.ts +2 -0
- package/src/__tests__/subagent-manager-notify.test.ts +2 -0
- package/src/__tests__/subagent-notify-parent.test.ts +2 -0
- package/src/__tests__/subagent-notify-stale-rebuild.test.ts +85 -0
- package/src/__tests__/subagent-tool-gate-mode.test.ts +4 -4
- package/src/__tests__/terminal-tools.test.ts +9 -9
- package/src/__tests__/tool-result-spool.test.ts +5 -8
- package/src/__tests__/usage-attribution.test.ts +26 -0
- package/src/acp/__tests__/acp-auth-marker-store.test.ts +191 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +262 -0
- package/src/acp/__tests__/acp-connect-card-state.test.ts +17 -0
- package/src/acp/__tests__/helpers/acp-history-db.ts +10 -3
- package/src/acp/acp-auth-marker-store.ts +196 -0
- package/src/acp/acp-claude-oauth.ts +154 -4
- package/src/acp/acp-connect-card-state.ts +49 -8
- package/src/acp/agent-process.ts +1 -0
- package/src/acp/auto-install.ts +1 -0
- package/src/acp/client-handler.ts +1 -0
- package/src/acp/prepare-agent-env.ts +79 -1
- package/src/acp/resolve-agent.ts +12 -0
- package/src/acp/session-manager.test.ts +183 -0
- package/src/acp/session-manager.ts +110 -6
- package/src/acp/types.ts +17 -0
- package/src/agent/attachments.ts +7 -2
- package/src/agent/loop.ts +59 -1
- package/src/api/events/contact-form-closed.ts +32 -0
- package/src/api/events/contact-record-request.ts +63 -0
- package/src/api/events/contact-request.ts +10 -3
- package/src/api/index.ts +20 -0
- package/src/api/responses/conversation-message.ts +20 -0
- package/src/api/responses/home.ts +101 -0
- package/src/approvals/AGENTS.md +1 -1
- package/src/approvals/guardian-card-withdrawal.ts +127 -30
- package/src/approvals/guardian-decision-primitive.ts +9 -0
- package/src/approvals/guardian-expiry-notifier.ts +22 -16
- package/src/approvals/guardian-request-status-sync.ts +98 -0
- package/src/avatar/svg-compositor.ts +1 -1
- package/src/avatar/traits-png-sync.ts +1 -1
- package/src/bundler/app-compiler.ts +1 -0
- package/src/bundler/compiler-tools.ts +1 -0
- package/src/bundler/package-resolver.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +183 -3
- package/src/calls/guardian-action-sweep.ts +6 -1
- package/src/calls/voice-session-bridge.ts +46 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +13 -18
- package/src/channels/gateway-guardian-requests.ts +37 -18
- package/src/cli/__tests__/contacts-record-prompt-cli.test.ts +329 -0
- package/src/cli/commands/__tests__/browser.test.ts +65 -1
- package/src/cli/commands/browser.ts +30 -10
- package/src/cli/commands/contacts.help.ts +173 -11
- package/src/cli/commands/contacts.ts +343 -4
- package/src/cli/commands/db/refresh.ts +1 -0
- package/src/cli/commands/oauth/index.help.ts +6 -3
- package/src/cli/commands/oauth/request.test.ts +37 -0
- package/src/cli/commands/oauth/request.ts +7 -6
- package/src/cli/lib/bundled-marketplace.json +1 -1
- package/src/cli/lib/install-from-github.ts +20 -19
- package/src/cli/lib/install-plugin-dependencies.ts +3 -5
- package/src/cli/lib/merge-plugin-tree.ts +1 -0
- package/src/cli/lib/publish-plugin.ts +4 -1
- package/src/config/__tests__/balanced-model-experiment.test.ts +63 -31
- package/src/config/__tests__/default-profile-catalog.test.ts +2 -2
- package/src/config/balanced-model-experiment.ts +15 -9
- package/src/config/bundled-skills/contacts/SKILL.md +61 -18
- package/src/config/default-profile-catalog.ts +12 -11
- package/src/config/feature-flag-registry.json +22 -13
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/live-voice.test.ts +13 -0
- package/src/config/schemas/live-voice.ts +7 -0
- package/src/config/schemas/sight.ts +45 -0
- package/src/config/skills.ts +2 -1
- package/src/contacts/__tests__/mirror-reconciler.test.ts +352 -0
- package/src/contacts/contact-store.ts +47 -4
- package/src/contacts/contacts-write.ts +5 -2
- package/src/contacts/mirror-reconciler.ts +210 -0
- package/src/contacts/mirror-tombstones.ts +42 -0
- package/src/contacts/notify-contacts-changed.ts +11 -2
- package/src/context/compactor.ts +47 -19
- package/src/context/tool-result-spool.ts +21 -14
- package/src/conversations/message-consolidation.ts +2 -2
- package/src/credential-health/credential-health-service.ts +37 -6
- package/src/daemon/conversation-agent-loop-handlers.ts +19 -2
- package/src/daemon/conversation-agent-loop.ts +100 -1
- package/src/daemon/conversation-media-retry.ts +11 -6
- package/src/daemon/conversation-messaging.ts +293 -30
- package/src/daemon/conversation-runtime-assembly.ts +67 -0
- package/src/daemon/conversation-sight-frames.ts +187 -0
- package/src/daemon/conversation-store.ts +18 -28
- package/src/daemon/conversation-surfaces.ts +12 -2
- package/src/daemon/conversation-tool-setup.ts +39 -10
- package/src/daemon/conversation.ts +159 -16
- package/src/daemon/daemon-control.ts +6 -17
- package/src/daemon/handlers/config-model.test.ts +1 -0
- package/src/daemon/handlers/conversations.ts +10 -11
- package/src/daemon/install-assistant-command.ts +1 -0
- package/src/daemon/lifecycle.ts +10 -13
- package/src/daemon/message-types/sync.ts +4 -0
- package/src/daemon/process-message.ts +2 -17
- package/src/daemon/reaction-history-render.test.ts +162 -0
- package/src/daemon/reaction-history-render.ts +115 -0
- package/src/daemon/reaction-record.test.ts +162 -0
- package/src/daemon/reaction-record.ts +83 -0
- package/src/daemon/shutdown-handlers.ts +0 -2
- package/src/daemon/summarize-boundary.ts +12 -0
- package/src/daemon/video-thumbnail.ts +1 -1
- package/src/home/feed-types.ts +4 -0
- package/src/home/feed-writer.ts +25 -0
- package/src/live-voice/__tests__/live-voice-attach-frame.test.ts +679 -0
- package/src/live-voice/__tests__/live-voice-attach-image.test.ts +8 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +637 -1
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +787 -0
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +347 -0
- package/src/live-voice/__tests__/live-voice-vad.test.ts +109 -0
- package/src/live-voice/live-voice-photo.ts +420 -65
- package/src/live-voice/live-voice-session.ts +311 -27
- package/src/live-voice/protocol.ts +147 -11
- package/src/messaging/provider-message-metadata.ts +7 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +113 -1
- package/src/messaging/providers/channel-transport.ts +37 -0
- package/src/messaging/providers/discord/api.ts +4 -4
- package/src/messaging/providers/discord/send.test.ts +177 -2
- package/src/messaging/providers/discord/send.ts +197 -10
- package/src/messaging/providers/discord/transport.ts +10 -0
- package/src/messaging/providers/index.ts +42 -0
- package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +129 -3
- package/src/messaging/providers/slack/__tests__/auth.test.ts +35 -0
- package/src/messaging/providers/slack/__tests__/message-content.test.ts +152 -0
- package/src/messaging/providers/slack/adapter.ts +112 -27
- package/src/messaging/providers/slack/api.test.ts +10 -0
- package/src/messaging/providers/slack/auth.ts +32 -6
- package/src/messaging/providers/slack/client.ts +0 -15
- package/src/messaging/providers/slack/message-content.ts +137 -0
- package/src/messaging/providers/slack/send.test.ts +43 -2
- package/src/messaging/providers/slack/send.ts +18 -5
- package/src/messaging/providers/slack/transport.ts +10 -0
- package/src/messaging/providers/slack/types.ts +21 -2
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -1
- package/src/messaging/providers/telegram-bot/send.test.ts +49 -2
- package/src/messaging/providers/telegram-bot/send.ts +41 -1
- package/src/messaging/providers/telegram-bot/transport.ts +10 -0
- package/src/messaging/providers/telegram-bot/withdraw.ts +13 -5
- package/src/messaging/reaction-envelopes.ts +79 -0
- package/src/monitoring/control.ts +2 -2
- package/src/monitoring/page-cache.ts +5 -1
- package/src/notifications/AGENTS.md +1 -1
- package/src/notifications/__tests__/guardian-feed-projection.test.ts +360 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +8 -0
- package/src/notifications/adapters/discord.ts +62 -9
- package/src/notifications/conversation-pairing.ts +31 -4
- package/src/notifications/emit-signal.ts +8 -5
- package/src/notifications/guardian-delivery-recorder.ts +47 -6
- package/src/notifications/guardian-feed-projection.ts +492 -0
- package/src/notifications/home-feed-side-effect.ts +59 -11
- package/src/oauth/byo-connection.test.ts +22 -0
- package/src/oauth/byo-connection.ts +11 -5
- package/src/oauth/connection.ts +3 -2
- package/src/oauth/platform-connection.test.ts +35 -0
- package/src/oauth/platform-connection.ts +18 -13
- package/src/oauth/scope-utils.ts +48 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/outbound-proxy/certs.ts +5 -4
- package/src/permissions/question-guardian-request.test.ts +7 -1
- package/src/persistence/attachments-store.ts +134 -31
- package/src/persistence/conversation-crud.ts +137 -4
- package/src/persistence/conversation-types.ts +109 -0
- package/src/persistence/db-connection.ts +3 -21
- package/src/persistence/delivery-crud.ts +42 -12
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +15 -2
- package/src/persistence/embeddings/embedding-local.ts +7 -112
- package/src/persistence/embeddings/qdrant-manager.ts +4 -1
- package/src/persistence/llm-request-log-store.ts +1 -1
- package/src/persistence/migrations/370-acp-session-history-auth-error-code.ts +36 -0
- package/src/persistence/migrations/371-acp-session-history-auth-error-credential.ts +40 -0
- package/src/persistence/migrations/372-create-acp-refused-credentials.ts +34 -0
- package/src/persistence/migrations/373-acp-auth-marker-index.ts +27 -0
- package/src/persistence/migrations/__tests__/370-acp-session-history-auth-error-code.test.ts +65 -0
- package/src/persistence/migrations/__tests__/371-acp-session-history-auth-error-credential.test.ts +68 -0
- package/src/persistence/migrations/__tests__/372-create-acp-refused-credentials.test.ts +64 -0
- package/src/persistence/migrations/__tests__/373-acp-auth-marker-index.test.ts +82 -0
- package/src/persistence/schema/acp.ts +25 -0
- package/src/persistence/steps.ts +8 -0
- package/src/platform/platform-patch-queue.test.ts +43 -41
- package/src/plugin-api/index.ts +12 -0
- package/src/plugins/defaults/compaction/window-manager.ts +2 -0
- package/src/plugins/defaults/image-recovery/recover.ts +12 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +166 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-provider-path.test.ts +3 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +3 -0
- package/src/plugins/defaults/memory/jobs-worker.ts +1 -2
- package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +53 -9
- package/src/plugins/defaults/memory/src/__tests__/memory-worker-routes.test.ts +4 -7
- package/src/plugins/defaults/memory/src/memory-worker-routes.ts +1 -1
- package/src/plugins/defaults/memory/v1/job-handlers/index-maintenance.ts +15 -1
- package/src/plugins/defaults/memory/worker-control.ts +1 -2
- package/src/providers/content-block-schema.ts +1 -0
- package/src/providers/fireworks/client.ts +9 -1
- package/src/providers/gemini/client.ts +22 -15
- package/src/providers/media-resolve.ts +24 -10
- package/src/providers/model-catalog.ts +91 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -5
- package/src/providers/retry.ts +16 -11
- package/src/providers/types.ts +56 -0
- package/src/routes/__tests__/control.test.ts +4 -3
- package/src/routes/control.ts +1 -1
- package/src/routes/route-host-client.ts +1 -1
- package/src/runtime/AGENTS.md +30 -12
- package/src/runtime/__tests__/agent-wake.test.ts +71 -6
- package/src/runtime/__tests__/guardian-form-registry.test.ts +242 -0
- package/src/runtime/access-request-helper.ts +15 -7
- package/src/runtime/agent-wake.ts +16 -4
- package/src/runtime/channel-approval-types.ts +8 -5
- package/src/runtime/channel-invite-transport.ts +2 -0
- package/src/runtime/channel-invite-transports/discord.ts +27 -0
- package/src/runtime/channel-readiness-service.ts +4 -4
- package/src/runtime/channel-reply-delivery.ts +72 -28
- package/src/runtime/guardian-action-service.ts +9 -5
- package/src/runtime/guardian-form-registry.ts +256 -0
- package/src/runtime/guardian-reply-router.ts +33 -14
- package/src/runtime/invite-instruction-generator.ts +14 -2
- package/src/runtime/migrations/vbundle-metadata-merge.ts +4 -7
- package/src/runtime/no-response.ts +13 -74
- package/src/runtime/routes/__tests__/acp-routes.test.ts +527 -0
- package/src/runtime/routes/__tests__/app-management-shared-apps.test.ts +74 -0
- package/src/runtime/routes/__tests__/contact-form-kinds.test.ts +22 -0
- package/src/runtime/routes/__tests__/contact-record-prompt-route.test.ts +303 -0
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +6 -0
- package/src/runtime/routes/__tests__/route-host-worker-routes.test.ts +3 -4
- package/src/runtime/routes/__tests__/schedule-worker-routes.test.ts +5 -6
- package/src/runtime/routes/acp-routes-list.test.ts +58 -0
- package/src/runtime/routes/acp-routes.ts +253 -47
- package/src/runtime/routes/app-management-routes.ts +80 -47
- package/src/runtime/routes/approval-routes.ts +9 -0
- package/src/runtime/routes/archive-utils.ts +6 -1
- package/src/runtime/routes/avatar-routes.ts +1 -1
- package/src/runtime/routes/channel-route-shared.ts +18 -20
- package/src/runtime/routes/contact-prompt-routes.ts +271 -105
- package/src/runtime/routes/conversation-routes.ts +41 -32
- package/src/runtime/routes/credential-prompt-routes.ts +10 -1
- package/src/runtime/routes/debug-bash-routes.ts +1 -0
- package/src/runtime/routes/guardian-expiry-sweep.ts +89 -31
- package/src/runtime/routes/guardian-form-routes.ts +115 -0
- package/src/runtime/routes/inbound-message-handler.ts +82 -29
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -3
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +6 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +6 -1
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +50 -17
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +97 -94
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/integrations/slack/channel.ts +39 -18
- package/src/runtime/routes/migration-routes.ts +1 -3
- package/src/runtime/routes/monitoring-routes.ts +0 -3
- package/src/runtime/routes/oauth-commands-routes.ts +18 -5
- package/src/runtime/routes/route-host-worker-routes.ts +1 -1
- package/src/runtime/routes/schedule-worker-routes.ts +1 -1
- package/src/runtime/tool-grant-request-helper.ts +4 -0
- package/src/schedule/run-script.ts +1 -0
- package/src/schedule/scheduler.ts +1 -1
- package/src/schedule/worker-control.ts +2 -2
- package/src/security/oauth2.ts +2 -11
- package/src/security/secure-keys.ts +55 -8
- package/src/skills/catalog-install.ts +7 -4
- package/src/skills/clawhub.ts +1 -0
- package/src/skills/inline-command-runner.ts +1 -0
- package/src/stt/__tests__/room-noise-floor.test.ts +108 -0
- package/src/stt/room-noise-floor.ts +140 -0
- package/src/subagent/notify.ts +49 -3
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/acp/spawn.ts +2 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +9 -0
- package/src/tools/browser/browser-execution.ts +10 -43
- package/src/tools/browser/browser-manager.ts +1 -0
- package/src/tools/browser/browser-status-constants.ts +6 -5
- package/src/tools/browser/runtime-check.ts +2 -0
- package/src/tools/channel/react-to-message.test.ts +193 -0
- package/src/tools/channel/react-to-message.ts +159 -0
- package/src/tools/credentials/metadata-store.ts +115 -19
- package/src/tools/credentials/store.ts +0 -2
- package/src/tools/filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-terminal/host-shell.test.ts +24 -0
- package/src/tools/host-terminal/host-shell.ts +11 -38
- package/src/tools/shared/filesystem/file-ops-service.ts +10 -7
- package/src/tools/shared/shell-output.test.ts +104 -0
- package/src/tools/shared/shell-output.ts +112 -39
- package/src/tools/skills/sandbox-runner.ts +1 -0
- package/src/tools/system/request-permission.test.ts +34 -0
- package/src/tools/system/request-permission.ts +29 -5
- package/src/tools/terminal/shell.test.ts +63 -0
- package/src/tools/terminal/shell.ts +12 -44
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/usage/attribution.ts +12 -4
- package/src/util/__tests__/worker-ownership.test.ts +77 -0
- package/src/util/__tests__/worker-process-command.test.ts +246 -1
- package/src/util/bun-runtime.ts +44 -13
- package/src/util/bundled-asset.ts +9 -1
- package/src/util/disk-usage.ts +1 -0
- package/src/util/file-use.ts +4 -1
- package/src/util/guardian-form-timeouts.ts +50 -0
- package/src/util/image-conversion.ts +1 -0
- package/src/util/oauth-request-body.test.ts +59 -0
- package/src/util/oauth-request-body.ts +56 -5
- package/src/util/platform.ts +69 -73
- package/src/util/process-table.ts +16 -5
- package/src/util/read-stdin.ts +5 -0
- package/src/util/spawn.ts +1 -0
- package/src/util/sqlite3-runtime.ts +25 -5
- package/src/util/worker-ownership.ts +193 -0
- package/src/util/worker-process.ts +375 -37
- package/src/workspace/git-service.ts +4 -12
- package/src/workspace/migrations/010-app-dir-rename.ts +1 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { slackMessageRawText } from "../message-content.js";
|
|
4
|
+
|
|
5
|
+
describe("slackMessageRawText", () => {
|
|
6
|
+
test("returns top-level text untouched when present", () => {
|
|
7
|
+
expect(
|
|
8
|
+
slackMessageRawText({
|
|
9
|
+
text: "hello <@U123>",
|
|
10
|
+
attachments: [{ fallback: "ignored" }],
|
|
11
|
+
}),
|
|
12
|
+
).toBe("hello <@U123>");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("whitespace-only text falls through to attachments", () => {
|
|
16
|
+
expect(
|
|
17
|
+
slackMessageRawText({
|
|
18
|
+
text: " \n ",
|
|
19
|
+
attachments: [{ fallback: "the fallback" }],
|
|
20
|
+
}),
|
|
21
|
+
).toBe("the fallback");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("prefers structured attachment fields over fallback", () => {
|
|
25
|
+
expect(
|
|
26
|
+
slackMessageRawText({
|
|
27
|
+
text: "",
|
|
28
|
+
attachments: [
|
|
29
|
+
{
|
|
30
|
+
fallback: "plain summary",
|
|
31
|
+
pretext: "build failed by <@U08E>",
|
|
32
|
+
author_name: "CI",
|
|
33
|
+
title: "CI Main Web",
|
|
34
|
+
title_link: "https://example.com/runs/1",
|
|
35
|
+
text: "3 tests failing",
|
|
36
|
+
fields: [
|
|
37
|
+
{ title: "Branch", value: "main" },
|
|
38
|
+
{ title: "", value: "value-only" },
|
|
39
|
+
],
|
|
40
|
+
footer: "workflow #12",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
}),
|
|
44
|
+
).toBe(
|
|
45
|
+
[
|
|
46
|
+
"build failed by <@U08E>",
|
|
47
|
+
"CI",
|
|
48
|
+
"CI Main Web (https://example.com/runs/1)",
|
|
49
|
+
"3 tests failing",
|
|
50
|
+
"Branch: main",
|
|
51
|
+
"value-only",
|
|
52
|
+
"workflow #12",
|
|
53
|
+
].join("\n"),
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("uses fallback when the attachment has no structured text", () => {
|
|
58
|
+
expect(
|
|
59
|
+
slackMessageRawText({
|
|
60
|
+
text: "",
|
|
61
|
+
attachments: [{ fallback: "only the fallback" }],
|
|
62
|
+
}),
|
|
63
|
+
).toBe("only the fallback");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("joins multiple attachments with newlines and drops empty ones", () => {
|
|
67
|
+
expect(
|
|
68
|
+
slackMessageRawText({
|
|
69
|
+
text: "",
|
|
70
|
+
attachments: [{ text: "first" }, {}, { fallback: "second" }],
|
|
71
|
+
}),
|
|
72
|
+
).toBe("first\nsecond");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("extracts section, header, and context block text ahead of attachments", () => {
|
|
76
|
+
expect(
|
|
77
|
+
slackMessageRawText({
|
|
78
|
+
text: "",
|
|
79
|
+
blocks: [
|
|
80
|
+
{
|
|
81
|
+
type: "header",
|
|
82
|
+
text: { type: "plain_text", text: "Release v1.2" },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: "section",
|
|
86
|
+
text: { type: "mrkdwn", text: "All green" },
|
|
87
|
+
fields: [
|
|
88
|
+
{ type: "mrkdwn", text: "Env: prod" },
|
|
89
|
+
{ type: "mrkdwn", text: "" },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{ type: "divider" },
|
|
93
|
+
{
|
|
94
|
+
type: "context",
|
|
95
|
+
elements: [
|
|
96
|
+
{ type: "mrkdwn", text: "deployed by <@U42>" },
|
|
97
|
+
{ type: "plain_text", text: "just now" },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
attachments: [{ fallback: "attachment tail" }],
|
|
102
|
+
}),
|
|
103
|
+
).toBe(
|
|
104
|
+
[
|
|
105
|
+
"Release v1.2",
|
|
106
|
+
"All green",
|
|
107
|
+
"Env: prod",
|
|
108
|
+
"deployed by <@U42> just now",
|
|
109
|
+
"attachment tail",
|
|
110
|
+
].join("\n"),
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("extracts markdown block text", () => {
|
|
115
|
+
expect(
|
|
116
|
+
slackMessageRawText({
|
|
117
|
+
text: "",
|
|
118
|
+
blocks: [
|
|
119
|
+
{ type: "markdown", text: "**Deploy failed** on `main`" },
|
|
120
|
+
{ type: "divider" },
|
|
121
|
+
],
|
|
122
|
+
}),
|
|
123
|
+
).toBe("**Deploy failed** on `main`");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("extracts blocks nested inside an attachment", () => {
|
|
127
|
+
expect(
|
|
128
|
+
slackMessageRawText({
|
|
129
|
+
text: "",
|
|
130
|
+
attachments: [
|
|
131
|
+
{
|
|
132
|
+
fallback: "plain summary",
|
|
133
|
+
blocks: [
|
|
134
|
+
{ type: "section", text: { type: "mrkdwn", text: "Run #7 red" } },
|
|
135
|
+
{ type: "markdown", text: "3 tests failing" },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
}),
|
|
140
|
+
).toBe("Run #7 red\n3 tests failing");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("returns empty string when nothing carries text", () => {
|
|
144
|
+
expect(
|
|
145
|
+
slackMessageRawText({
|
|
146
|
+
text: "",
|
|
147
|
+
blocks: [{ type: "divider" }],
|
|
148
|
+
attachments: [{}],
|
|
149
|
+
}),
|
|
150
|
+
).toBe("");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
isPrivateConversation,
|
|
39
39
|
slackUserDisplayName,
|
|
40
40
|
} from "./conversation-utils.js";
|
|
41
|
+
import { slackMessageRawText } from "./message-content.js";
|
|
41
42
|
import type {
|
|
42
43
|
SlackConversation,
|
|
43
44
|
SlackMessage,
|
|
@@ -79,6 +80,15 @@ const userInfoCache = new Map<string, Promise<SlackUserInfoLookupResult>>();
|
|
|
79
80
|
*/
|
|
80
81
|
const channelNameCache = new Map<string, Promise<string | undefined>>();
|
|
81
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Cache resolved bot display names (`bots.info`) for bot-authored rows that
|
|
85
|
+
* carry only a `bot_id`, same lifetime and keying discipline as
|
|
86
|
+
* {@link userInfoCache}. Holds `undefined` for bots this auth provably
|
|
87
|
+
* cannot resolve so a wall of webhook history doesn't re-fire a doomed
|
|
88
|
+
* lookup per message batch.
|
|
89
|
+
*/
|
|
90
|
+
const botNameCache = new Map<string, Promise<string | undefined>>();
|
|
91
|
+
|
|
82
92
|
/**
|
|
83
93
|
* Cached auth resolved during resolveConnection(), split by direction.
|
|
84
94
|
*
|
|
@@ -331,42 +341,113 @@ function slackUserInfoCacheKey(
|
|
|
331
341
|
}
|
|
332
342
|
|
|
333
343
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* retries.
|
|
344
|
+
* Shared resolve-once discipline for the Slack directory name caches
|
|
345
|
+
* (channel and bot names): concurrent callers share the in-flight promise,
|
|
346
|
+
* successes and provably-permanent failures stay cached, and transient
|
|
347
|
+
* failures are evicted so a later batch retries.
|
|
338
348
|
*/
|
|
339
|
-
async function
|
|
340
|
-
|
|
341
|
-
|
|
349
|
+
async function resolveCachedSlackName(
|
|
350
|
+
cache: Map<string, Promise<string | undefined>>,
|
|
351
|
+
cacheKey: string,
|
|
352
|
+
lookup: () => Promise<string | undefined>,
|
|
353
|
+
isPermanentFailure: (err: unknown) => boolean,
|
|
342
354
|
): Promise<string | undefined> {
|
|
343
|
-
|
|
344
|
-
return undefined;
|
|
345
|
-
}
|
|
346
|
-
const cacheKey = `${slackAuthCacheScope(auth)}:channel:${channelId}`;
|
|
347
|
-
const cached = channelNameCache.get(cacheKey);
|
|
355
|
+
const cached = cache.get(cacheKey);
|
|
348
356
|
if (cached) {
|
|
349
357
|
return cached;
|
|
350
358
|
}
|
|
351
359
|
|
|
352
|
-
const resolved =
|
|
353
|
-
(
|
|
360
|
+
const resolved = lookup().then(
|
|
361
|
+
(name) => name,
|
|
354
362
|
(err: unknown) => {
|
|
355
363
|
// Cache the definitive "this auth cannot resolve it" answers; drop
|
|
356
364
|
// everything else so transient failures retry on the next batch.
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
(err.category === "channel_not_found" || err.category === "permission");
|
|
360
|
-
if (!permanent) {
|
|
361
|
-
channelNameCache.delete(cacheKey);
|
|
365
|
+
if (!isPermanentFailure(err)) {
|
|
366
|
+
cache.delete(cacheKey);
|
|
362
367
|
}
|
|
363
368
|
return undefined;
|
|
364
369
|
},
|
|
365
370
|
);
|
|
366
|
-
|
|
371
|
+
cache.set(cacheKey, resolved);
|
|
367
372
|
return resolved;
|
|
368
373
|
}
|
|
369
374
|
|
|
375
|
+
/**
|
|
376
|
+
* Resolve a channel's display name for inline mention rendering, cached per
|
|
377
|
+
* auth scope. Returns undefined when the channel has no name (DMs) or this
|
|
378
|
+
* auth cannot see it.
|
|
379
|
+
*/
|
|
380
|
+
async function resolveChannelName(
|
|
381
|
+
auth: OAuthConnection | string,
|
|
382
|
+
channelId: string,
|
|
383
|
+
): Promise<string | undefined> {
|
|
384
|
+
if (!channelId) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
return resolveCachedSlackName(
|
|
388
|
+
channelNameCache,
|
|
389
|
+
`${slackAuthCacheScope(auth)}:channel:${channelId}`,
|
|
390
|
+
async () =>
|
|
391
|
+
trimNonEmpty(
|
|
392
|
+
(await slack.conversationInfo(auth, channelId)).channel.name,
|
|
393
|
+
),
|
|
394
|
+
(err) =>
|
|
395
|
+
err instanceof SlackApiError &&
|
|
396
|
+
(err.category === "channel_not_found" || err.category === "permission"),
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Resolve a bot's display name via `bots.info`, cached per auth scope.
|
|
402
|
+
* Returns undefined when this auth cannot resolve it.
|
|
403
|
+
*/
|
|
404
|
+
async function resolveBotName(
|
|
405
|
+
auth: OAuthConnection | string,
|
|
406
|
+
botId: string,
|
|
407
|
+
): Promise<string | undefined> {
|
|
408
|
+
return resolveCachedSlackName(
|
|
409
|
+
botNameCache,
|
|
410
|
+
`${slackAuthCacheScope(auth)}:bot:${botId}`,
|
|
411
|
+
async () => trimNonEmpty((await slack.botsInfo(auth, botId)).bot.name),
|
|
412
|
+
(err) =>
|
|
413
|
+
err instanceof SlackApiError &&
|
|
414
|
+
(err.slackError === "bot_not_found" || err.category === "permission"),
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Sender info for a bot-authored row (no `user` to look up). The message's
|
|
420
|
+
* own `username` (bot_message subtype) or `bot_profile.name` answers without
|
|
421
|
+
* I/O; `bots.info` covers rows carrying only a `bot_id`, with the id itself
|
|
422
|
+
* as the last resort so the sender is at least attributable.
|
|
423
|
+
*
|
|
424
|
+
* `batchBotNames` pins the first lookup attempt per bot id for the caller's
|
|
425
|
+
* batch: the module cache evicts transient failures (so a later batch
|
|
426
|
+
* retries), and without the pin a history page full of one webhook's rows
|
|
427
|
+
* would re-fire a doomed `bots.info` cycle per row during a Slack outage.
|
|
428
|
+
*/
|
|
429
|
+
async function resolveBotSenderInfo(
|
|
430
|
+
auth: OAuthConnection | string,
|
|
431
|
+
msg: SlackMessage,
|
|
432
|
+
batchBotNames: Map<string, Promise<string | undefined>>,
|
|
433
|
+
): Promise<NormalizedSlackUserInfo> {
|
|
434
|
+
const fromMessage =
|
|
435
|
+
trimNonEmpty(msg.username) ?? trimNonEmpty(msg.bot_profile?.name);
|
|
436
|
+
if (fromMessage) {
|
|
437
|
+
return { displayName: fromMessage };
|
|
438
|
+
}
|
|
439
|
+
const botId = msg.bot_id?.trim();
|
|
440
|
+
if (!botId) {
|
|
441
|
+
return { displayName: "unknown" };
|
|
442
|
+
}
|
|
443
|
+
let pending = batchBotNames.get(botId);
|
|
444
|
+
if (pending === undefined) {
|
|
445
|
+
pending = resolveBotName(auth, botId);
|
|
446
|
+
batchBotNames.set(botId, pending);
|
|
447
|
+
}
|
|
448
|
+
return { displayName: (await pending) ?? botId };
|
|
449
|
+
}
|
|
450
|
+
|
|
370
451
|
function normalizeSlackUserInfo(
|
|
371
452
|
user: SlackUser,
|
|
372
453
|
contactDisplayName: string | undefined,
|
|
@@ -398,6 +479,7 @@ function trimNonEmpty(value: unknown): string | undefined {
|
|
|
398
479
|
export function __resetSlackMentionCachesForTests(): void {
|
|
399
480
|
userInfoCache.clear();
|
|
400
481
|
channelNameCache.clear();
|
|
482
|
+
botNameCache.clear();
|
|
401
483
|
}
|
|
402
484
|
|
|
403
485
|
function slackUserInfoMetadata(
|
|
@@ -529,19 +611,22 @@ async function mapSlackMessages(
|
|
|
529
611
|
channelId: string,
|
|
530
612
|
slackMessages: SlackMessage[],
|
|
531
613
|
): Promise<Message[]> {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
);
|
|
614
|
+
// Raw text may come from attachments/blocks (bot and webhook posts), so
|
|
615
|
+
// mention labels are built from the same derived strings that get rendered.
|
|
616
|
+
const rawTexts = slackMessages.map((msg) => slackMessageRawText(msg));
|
|
617
|
+
const labels = await buildMentionLabels(auth, rawTexts);
|
|
618
|
+
const batchBotNames = new Map<string, Promise<string | undefined>>();
|
|
536
619
|
const messages: Message[] = [];
|
|
537
|
-
for (const msg of slackMessages) {
|
|
538
|
-
const senderInfo =
|
|
620
|
+
for (const [i, msg] of slackMessages.entries()) {
|
|
621
|
+
const senderInfo = msg.user
|
|
622
|
+
? await resolveUserInfo(auth, msg.user)
|
|
623
|
+
: await resolveBotSenderInfo(auth, msg, batchBotNames);
|
|
539
624
|
messages.push(
|
|
540
625
|
mapMessage(
|
|
541
626
|
msg,
|
|
542
627
|
channelId,
|
|
543
628
|
senderInfo,
|
|
544
|
-
renderSlackTextForModel(
|
|
629
|
+
renderSlackTextForModel(rawTexts[i], labels),
|
|
545
630
|
),
|
|
546
631
|
);
|
|
547
632
|
}
|
|
@@ -28,6 +28,16 @@ mock.module("../../../security/secure-keys.js", () => ({
|
|
|
28
28
|
}),
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
|
+
// With `userTokenStored` false, `resolveSlackAuth` looks for a `slack` OAuth
|
|
32
|
+
// connection before falling back to the bot token. This suite builds no
|
|
33
|
+
// database, so the lookup has to be answered here; spread for the same reason
|
|
34
|
+
// the secure-keys mock above does.
|
|
35
|
+
const realOauthStore = await import("../../../oauth/oauth-store.js");
|
|
36
|
+
mock.module("../../../oauth/oauth-store.js", () => ({
|
|
37
|
+
...realOauthStore,
|
|
38
|
+
getConnectionByProvider: () => undefined,
|
|
39
|
+
}));
|
|
40
|
+
|
|
31
41
|
const { appendSlackStream, getSlackConversationInfo, startSlackStream } =
|
|
32
42
|
await import("./api.js");
|
|
33
43
|
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
* any caller read the installer's channels or post as them. Also used for
|
|
22
22
|
* content the assistant posts as itself.
|
|
23
23
|
*
|
|
24
|
-
* - "user"
|
|
25
|
-
* its wider reach (channels the owner is in but
|
|
26
|
-
* `search.messages`, which only a user token can call)
|
|
24
|
+
* - "user": act as the assistant's owner, using whichever of the installer's
|
|
25
|
+
* own tokens is present for its wider reach (channels the owner is in but
|
|
26
|
+
* the bot isn't; and `search.messages`, which only a user token can call):
|
|
27
|
+
* the channel's pasted user token, else a `slack` OAuth connection, else
|
|
28
|
+
* the bot token. Scoped to the
|
|
27
29
|
* in-conversation messaging adapter, where the assistant is acting for its
|
|
28
30
|
* own owner within a trust-classified conversation — NOT the edge-reachable
|
|
29
31
|
* control-plane routes above. Falls back to the bot token when no user
|
|
@@ -57,12 +59,36 @@ export async function resolveSlackAuth(
|
|
|
57
59
|
);
|
|
58
60
|
if (botToken) {
|
|
59
61
|
if (identity === "user") {
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
+
// Three rungs, widest reach first. The pasted channel user token and the
|
|
63
|
+
// `slack` OAuth connection are the same thing obtained two ways: the
|
|
64
|
+
// installer's own token, carrying the channel and search scopes a bot
|
|
65
|
+
// token cannot. Falling straight to the bot from a missing paste would
|
|
66
|
+
// drop `search.messages`, which only a user token can call, while a
|
|
67
|
+
// usable one sat in the OAuth connection.
|
|
62
68
|
const userToken = await getSecureKeyAsync(
|
|
63
69
|
credentialKey("slack_channel", "user_token"),
|
|
64
70
|
);
|
|
65
|
-
|
|
71
|
+
if (userToken) {
|
|
72
|
+
return userToken;
|
|
73
|
+
}
|
|
74
|
+
// The connection holds the installer's user token: this provider always
|
|
75
|
+
// requests `user_scope`, and `exchangeCodeForTokens` stores the nested
|
|
76
|
+
// `authed_user` token whenever Slack returns one. An install that
|
|
77
|
+
// granted no user scopes leaves a bot token there instead, which is the
|
|
78
|
+
// same thing the next line falls back to, so the wrong guess costs
|
|
79
|
+
// nothing.
|
|
80
|
+
if (getConnectionByProvider("slack")) {
|
|
81
|
+
try {
|
|
82
|
+
return await resolveOAuthConnection("slack", {
|
|
83
|
+
account: opts.account,
|
|
84
|
+
});
|
|
85
|
+
} catch {
|
|
86
|
+
// Unlike the legacy branch below, this one has somewhere to go: a
|
|
87
|
+
// revoked or unrefreshable connection degrades to the bot token
|
|
88
|
+
// rather than failing a resolve the bot can still answer.
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return botToken;
|
|
66
92
|
}
|
|
67
93
|
return botToken;
|
|
68
94
|
}
|
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
SlackConversationsListResponse,
|
|
23
23
|
SlackConversationsOpenResponse,
|
|
24
24
|
SlackPostMessageResponse,
|
|
25
|
-
SlackReactionsAddResponse,
|
|
26
25
|
SlackSearchMessagesResponse,
|
|
27
26
|
SlackUserInfoResponse,
|
|
28
27
|
SlackUsersListResponse,
|
|
@@ -222,20 +221,6 @@ export async function searchMessages(
|
|
|
222
221
|
);
|
|
223
222
|
}
|
|
224
223
|
|
|
225
|
-
export async function addReaction(
|
|
226
|
-
connectionOrToken: OAuthConnection | string,
|
|
227
|
-
channel: string,
|
|
228
|
-
timestamp: string,
|
|
229
|
-
name: string,
|
|
230
|
-
): Promise<SlackReactionsAddResponse> {
|
|
231
|
-
return request<SlackReactionsAddResponse>(
|
|
232
|
-
connectionOrToken,
|
|
233
|
-
"reactions.add",
|
|
234
|
-
undefined,
|
|
235
|
-
{ channel, timestamp, name },
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
224
|
export async function listUsers(
|
|
240
225
|
connectionOrToken: OAuthConnection | string,
|
|
241
226
|
limit = 200,
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback text extraction for Slack messages whose content lives outside
|
|
3
|
+
* the top-level `text` field.
|
|
4
|
+
*
|
|
5
|
+
* Bot and incoming-webhook posts (CI alerts, app notifications) routinely
|
|
6
|
+
* ship an empty `text` with the visible content in legacy `attachments` or
|
|
7
|
+
* Block Kit `blocks`; Slack's own clients render those. A mapping that reads
|
|
8
|
+
* only `text` turns such a message into an empty row, which downstream
|
|
9
|
+
* surfaces (transcripts, channel references) present as missing content.
|
|
10
|
+
*
|
|
11
|
+
* Output is raw Slack mrkdwn: mention/link tokens inside attachments and
|
|
12
|
+
* blocks are preserved so callers can route the result through the same
|
|
13
|
+
* `renderSlackTextForModel` pass as ordinary message text.
|
|
14
|
+
*/
|
|
15
|
+
import type {
|
|
16
|
+
AnyBlock,
|
|
17
|
+
ContextBlock,
|
|
18
|
+
HeaderBlock,
|
|
19
|
+
MarkdownBlock,
|
|
20
|
+
MessageAttachment,
|
|
21
|
+
SectionBlock,
|
|
22
|
+
} from "@slack/types";
|
|
23
|
+
|
|
24
|
+
import type { SlackMessage } from "./types.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The model-facing raw mrkdwn for a Slack message: `text` when present,
|
|
28
|
+
* otherwise content derived from `blocks` and `attachments` (in that order,
|
|
29
|
+
* matching how Slack renders a message body above its attachments).
|
|
30
|
+
*/
|
|
31
|
+
export function slackMessageRawText(
|
|
32
|
+
msg: Pick<SlackMessage, "text" | "attachments" | "blocks">,
|
|
33
|
+
): string {
|
|
34
|
+
if (msg.text?.trim()) {
|
|
35
|
+
return msg.text;
|
|
36
|
+
}
|
|
37
|
+
const parts = [
|
|
38
|
+
...slackBlocksText(msg.blocks),
|
|
39
|
+
...(msg.attachments ?? []).map(slackAttachmentText),
|
|
40
|
+
].filter((part) => part.length > 0);
|
|
41
|
+
return parts.join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function slackAttachmentText(attachment: MessageAttachment): string {
|
|
45
|
+
// An attachment carrying Block Kit blocks renders them as its body, so
|
|
46
|
+
// they lead; the legacy fields still contribute when both are present.
|
|
47
|
+
const blockParts = slackBlocksText(attachment.blocks);
|
|
48
|
+
const title = trimmed(attachment.title);
|
|
49
|
+
const titleLink = trimmed(attachment.title_link);
|
|
50
|
+
const fieldLines = (attachment.fields ?? []).map((field) => {
|
|
51
|
+
const fieldTitle = trimmed(field.title);
|
|
52
|
+
const fieldValue = trimmed(field.value);
|
|
53
|
+
if (fieldTitle && fieldValue) {
|
|
54
|
+
return `${fieldTitle}: ${fieldValue}`;
|
|
55
|
+
}
|
|
56
|
+
return fieldTitle ?? fieldValue ?? "";
|
|
57
|
+
});
|
|
58
|
+
const structured = [
|
|
59
|
+
...blockParts,
|
|
60
|
+
trimmed(attachment.pretext),
|
|
61
|
+
trimmed(attachment.author_name),
|
|
62
|
+
title && titleLink ? `${title} (${titleLink})` : title,
|
|
63
|
+
trimmed(attachment.text),
|
|
64
|
+
...fieldLines,
|
|
65
|
+
trimmed(attachment.footer),
|
|
66
|
+
].filter((part): part is string => !!part);
|
|
67
|
+
if (structured.length > 0) {
|
|
68
|
+
return structured.join("\n");
|
|
69
|
+
}
|
|
70
|
+
return trimmed(attachment.fallback) ?? "";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isSectionBlock(block: AnyBlock): block is SectionBlock {
|
|
74
|
+
return block.type === "section";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isHeaderBlock(block: AnyBlock): block is HeaderBlock {
|
|
78
|
+
return block.type === "header";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isContextBlock(block: AnyBlock): block is ContextBlock {
|
|
82
|
+
return block.type === "context";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isMarkdownBlock(block: AnyBlock): block is MarkdownBlock {
|
|
86
|
+
return block.type === "markdown";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Text carried by the block types that hold a message body's copy. Other
|
|
91
|
+
* block types contribute nothing: images/dividers/actions are non-textual,
|
|
92
|
+
* and `rich_text` (deeply nested; produced for user-typed messages, which
|
|
93
|
+
* carry the equivalent top-level `text`) is intentionally not walked.
|
|
94
|
+
*/
|
|
95
|
+
function slackBlocksText(blocks: readonly AnyBlock[] | undefined): string[] {
|
|
96
|
+
const parts: string[] = [];
|
|
97
|
+
for (const block of blocks ?? []) {
|
|
98
|
+
if (isSectionBlock(block)) {
|
|
99
|
+
const text = trimmed(block.text?.text);
|
|
100
|
+
if (text) {
|
|
101
|
+
parts.push(text);
|
|
102
|
+
}
|
|
103
|
+
for (const field of block.fields ?? []) {
|
|
104
|
+
const fieldText = trimmed(field.text);
|
|
105
|
+
if (fieldText) {
|
|
106
|
+
parts.push(fieldText);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} else if (isHeaderBlock(block)) {
|
|
110
|
+
const text = trimmed(block.text.text);
|
|
111
|
+
if (text) {
|
|
112
|
+
parts.push(text);
|
|
113
|
+
}
|
|
114
|
+
} else if (isContextBlock(block)) {
|
|
115
|
+
const contextText = block.elements
|
|
116
|
+
.map((element) =>
|
|
117
|
+
"text" in element ? trimmed(element.text) : undefined,
|
|
118
|
+
)
|
|
119
|
+
.filter((text): text is string => !!text)
|
|
120
|
+
.join(" ");
|
|
121
|
+
if (contextText) {
|
|
122
|
+
parts.push(contextText);
|
|
123
|
+
}
|
|
124
|
+
} else if (isMarkdownBlock(block)) {
|
|
125
|
+
const text = trimmed(block.text);
|
|
126
|
+
if (text) {
|
|
127
|
+
parts.push(text);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return parts;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function trimmed(value: string | undefined): string | undefined {
|
|
135
|
+
const result = value?.trim();
|
|
136
|
+
return result ? result : undefined;
|
|
137
|
+
}
|
|
@@ -28,8 +28,12 @@ mock.module("./api.js", () => ({
|
|
|
28
28
|
// the (unmocked) shared transport, so thrown test errors must be real
|
|
29
29
|
// instances or every branch under test would silently take the generic path.
|
|
30
30
|
const { SlackApiError } = await import("./web-api-transport.js");
|
|
31
|
-
const {
|
|
32
|
-
|
|
31
|
+
const {
|
|
32
|
+
sendSlackAgentSessionStatus,
|
|
33
|
+
sendSlackReaction,
|
|
34
|
+
sendSlackReply,
|
|
35
|
+
updateSlackMessage,
|
|
36
|
+
} = await import("./send.js");
|
|
33
37
|
|
|
34
38
|
describe("sendSlackAgentSessionStatus", () => {
|
|
35
39
|
const threadTs = "1700000000.000100";
|
|
@@ -137,6 +141,43 @@ describe("sendSlackAgentSessionStatus", () => {
|
|
|
137
141
|
});
|
|
138
142
|
});
|
|
139
143
|
|
|
144
|
+
describe("sendSlackReaction", () => {
|
|
145
|
+
test("adds with the bare emoji name, colons stripped", async () => {
|
|
146
|
+
const result = await sendSlackReaction("C1", ":tada:", "123.456", "add");
|
|
147
|
+
expect(result).toEqual({ ok: true });
|
|
148
|
+
expect(callSlackApiMock).toHaveBeenCalledWith("reactions.add", {
|
|
149
|
+
channel: "C1",
|
|
150
|
+
name: "tada",
|
|
151
|
+
timestamp: "123.456",
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("remove routes to reactions.remove", async () => {
|
|
156
|
+
await sendSlackReaction("C1", "tada", "123.456", "remove");
|
|
157
|
+
expect(callSlackApiMock).toHaveBeenCalledWith("reactions.remove", {
|
|
158
|
+
channel: "C1",
|
|
159
|
+
name: "tada",
|
|
160
|
+
timestamp: "123.456",
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("already_reacted reads as success: the end state holds", async () => {
|
|
165
|
+
callSlackApiMock.mockImplementationOnce(async () => {
|
|
166
|
+
throw new SlackApiError("already_reacted");
|
|
167
|
+
});
|
|
168
|
+
const result = await sendSlackReaction("C1", "tada", "123.456", "add");
|
|
169
|
+
expect(result).toEqual({ ok: true });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("any other failure reports ok false without throwing", async () => {
|
|
173
|
+
callSlackApiMock.mockImplementationOnce(async () => {
|
|
174
|
+
throw new SlackApiError("channel_not_found");
|
|
175
|
+
});
|
|
176
|
+
const result = await sendSlackReaction("C1", "tada", "123.456", "add");
|
|
177
|
+
expect(result).toEqual({ ok: false });
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
140
181
|
describe("updateSlackMessage", () => {
|
|
141
182
|
const messageTs = "1700000000.000100";
|
|
142
183
|
const blocks: KnownBlock[] = [
|