@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
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* attachments, by calling the Discord REST API directly via ./api.ts.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type {
|
|
9
|
+
ApprovalUIMetadata,
|
|
10
|
+
ChannelDeliveryResult,
|
|
11
|
+
} from "@vellumai/gateway-client";
|
|
12
|
+
|
|
8
13
|
import { getAttachmentContent } from "../../../persistence/attachments-store.js";
|
|
9
14
|
import type { RuntimeAttachmentMetadata } from "../../../runtime/http-types.js";
|
|
10
15
|
import { getLogger } from "../../../util/logger.js";
|
|
@@ -42,6 +47,90 @@ const DISCORD_ALLOWED_MENTIONS = { parse: ["users"] } as const;
|
|
|
42
47
|
*/
|
|
43
48
|
const DISCORD_MAX_ATTACHMENT_BYTES = 100 * 1024 * 1024;
|
|
44
49
|
|
|
50
|
+
// Component wire values, from Discord's message-components table.
|
|
51
|
+
const DISCORD_COMPONENT_ACTION_ROW = 1;
|
|
52
|
+
const DISCORD_COMPONENT_BUTTON = 2;
|
|
53
|
+
const DISCORD_BUTTON_STYLE_PRIMARY = 1;
|
|
54
|
+
const DISCORD_BUTTON_STYLE_SECONDARY = 2;
|
|
55
|
+
const DISCORD_BUTTON_STYLE_DANGER = 4;
|
|
56
|
+
/** Discord caps a component `custom_id` at 100 characters. */
|
|
57
|
+
const DISCORD_MAX_CUSTOM_ID_CHARS = 100;
|
|
58
|
+
/** Discord caps a button label at 80 characters. */
|
|
59
|
+
const DISCORD_MAX_BUTTON_LABEL_CHARS = 80;
|
|
60
|
+
/** Discord caps an action row at five buttons. */
|
|
61
|
+
const DISCORD_MAX_BUTTONS_PER_ROW = 5;
|
|
62
|
+
|
|
63
|
+
interface DiscordButtonComponent {
|
|
64
|
+
type: typeof DISCORD_COMPONENT_BUTTON;
|
|
65
|
+
style: number;
|
|
66
|
+
label: string;
|
|
67
|
+
custom_id: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface DiscordActionRow {
|
|
71
|
+
type: typeof DISCORD_COMPONENT_ACTION_ROW;
|
|
72
|
+
components: DiscordButtonComponent[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Translate a surface-agnostic emphasis into Discord's button style value. */
|
|
76
|
+
function discordStyleForEmphasis(
|
|
77
|
+
emphasis: "primary" | "secondary" | "destructive",
|
|
78
|
+
): number {
|
|
79
|
+
switch (emphasis) {
|
|
80
|
+
case "primary":
|
|
81
|
+
return DISCORD_BUTTON_STYLE_PRIMARY;
|
|
82
|
+
case "destructive":
|
|
83
|
+
return DISCORD_BUTTON_STYLE_DANGER;
|
|
84
|
+
case "secondary":
|
|
85
|
+
return DISCORD_BUTTON_STYLE_SECONDARY;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build the action rows for an approval card. Each button's `custom_id` is
|
|
91
|
+
* the shared `apr:<requestId>:<action>` callback convention, which the
|
|
92
|
+
* gateway forwards verbatim as the button event's `callbackData`. Styling
|
|
93
|
+
* mirrors the Slack card: an action's own `emphasis` wins, otherwise the
|
|
94
|
+
* first action is primary and `reject` is danger. Discord requires a style
|
|
95
|
+
* on every button, so the remainder are explicitly secondary.
|
|
96
|
+
*
|
|
97
|
+
* Throws when a `custom_id` would exceed Discord's cap: the caller falls
|
|
98
|
+
* back to the plain-text card rather than sending a button that could not
|
|
99
|
+
* round-trip its request id.
|
|
100
|
+
*/
|
|
101
|
+
function buildDiscordApprovalComponents(
|
|
102
|
+
approval: ApprovalUIMetadata,
|
|
103
|
+
): DiscordActionRow[] {
|
|
104
|
+
const buttons = approval.actions.map((action, index) => {
|
|
105
|
+
const customId = `apr:${approval.requestId}:${action.id}`;
|
|
106
|
+
if (customId.length > DISCORD_MAX_CUSTOM_ID_CHARS) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`custom_id for action "${action.id}" is ${customId.length} characters, exceeding Discord's ${DISCORD_MAX_CUSTOM_ID_CHARS}-character limit`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
type: DISCORD_COMPONENT_BUTTON,
|
|
113
|
+
style: action.emphasis
|
|
114
|
+
? discordStyleForEmphasis(action.emphasis)
|
|
115
|
+
: action.id === "reject"
|
|
116
|
+
? DISCORD_BUTTON_STYLE_DANGER
|
|
117
|
+
: index === 0
|
|
118
|
+
? DISCORD_BUTTON_STYLE_PRIMARY
|
|
119
|
+
: DISCORD_BUTTON_STYLE_SECONDARY,
|
|
120
|
+
label: action.label.slice(0, DISCORD_MAX_BUTTON_LABEL_CHARS),
|
|
121
|
+
custom_id: customId,
|
|
122
|
+
} satisfies DiscordButtonComponent;
|
|
123
|
+
});
|
|
124
|
+
const rows: DiscordActionRow[] = [];
|
|
125
|
+
for (let i = 0; i < buttons.length; i += DISCORD_MAX_BUTTONS_PER_ROW) {
|
|
126
|
+
rows.push({
|
|
127
|
+
type: DISCORD_COMPONENT_ACTION_ROW,
|
|
128
|
+
components: buttons.slice(i, i + DISCORD_MAX_BUTTONS_PER_ROW),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
|
|
45
134
|
/** Send target for one Discord delivery. */
|
|
46
135
|
export interface DiscordSendTarget {
|
|
47
136
|
/**
|
|
@@ -56,6 +145,38 @@ function messagesRoute(target: DiscordSendTarget): string {
|
|
|
56
145
|
return `/channels/${encodeURIComponent(target.channelId)}/messages`;
|
|
57
146
|
}
|
|
58
147
|
|
|
148
|
+
/**
|
|
149
|
+
* A multi-chunk send that failed after at least one chunk was posted. The
|
|
150
|
+
* already-delivered chunks cannot be unsent, so a caller with a fallback
|
|
151
|
+
* must not replay the whole text; the undelivered remainder is what is
|
|
152
|
+
* still owed to the reader.
|
|
153
|
+
*/
|
|
154
|
+
export class DiscordPartialSendError extends Error {
|
|
155
|
+
readonly chunksSent: number;
|
|
156
|
+
readonly remainingText: string;
|
|
157
|
+
readonly lastMessageId?: string;
|
|
158
|
+
|
|
159
|
+
constructor(
|
|
160
|
+
cause: unknown,
|
|
161
|
+
chunksSent: number,
|
|
162
|
+
remainingText: string,
|
|
163
|
+
lastMessageId?: string,
|
|
164
|
+
) {
|
|
165
|
+
super(
|
|
166
|
+
`Discord send failed after ${chunksSent} chunk(s): ${
|
|
167
|
+
cause instanceof Error ? cause.message : String(cause)
|
|
168
|
+
}`,
|
|
169
|
+
{ cause },
|
|
170
|
+
);
|
|
171
|
+
this.name = "DiscordPartialSendError";
|
|
172
|
+
this.chunksSent = chunksSent;
|
|
173
|
+
this.remainingText = remainingText;
|
|
174
|
+
if (lastMessageId !== undefined) {
|
|
175
|
+
this.lastMessageId = lastMessageId;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
59
180
|
/** Outcome of a Discord reply send. */
|
|
60
181
|
export interface DiscordSendResult {
|
|
61
182
|
/**
|
|
@@ -108,6 +229,11 @@ export async function editDiscordMessage(
|
|
|
108
229
|
{
|
|
109
230
|
content: options?.emphasis === "muted" ? mutedText(text) : text,
|
|
110
231
|
allowed_mentions: DISCORD_ALLOWED_MENTIONS,
|
|
232
|
+
// A PATCH that omits `components` keeps whatever the message carries,
|
|
233
|
+
// and every edit through this path states a final text: a card that
|
|
234
|
+
// has been answered or withdrawn must not keep live buttons. Always
|
|
235
|
+
// stripping makes that an invariant rather than a caller's chore.
|
|
236
|
+
components: [],
|
|
111
237
|
},
|
|
112
238
|
);
|
|
113
239
|
log.debug(
|
|
@@ -123,23 +249,46 @@ export async function editDiscordMessage(
|
|
|
123
249
|
export async function sendDiscordReply(
|
|
124
250
|
target: DiscordSendTarget,
|
|
125
251
|
text: string,
|
|
252
|
+
approval?: ApprovalUIMetadata,
|
|
126
253
|
): Promise<DiscordSendResult> {
|
|
127
254
|
const chunks = renderDiscordMessages(text);
|
|
128
255
|
if (chunks.length === 0) {
|
|
129
256
|
return {};
|
|
130
257
|
}
|
|
131
258
|
|
|
259
|
+
// Buttons ride the final chunk: its id is the one recorded on the delivery
|
|
260
|
+
// row, so the message a press arrives on is the message the row can find.
|
|
261
|
+
const components = approval ? buildDiscordApprovalComponents(approval) : [];
|
|
262
|
+
|
|
132
263
|
let lastMessageId: string | undefined;
|
|
133
|
-
for (const chunk of chunks) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
264
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
265
|
+
try {
|
|
266
|
+
const sent = await callDiscordApi<DiscordMessage>(
|
|
267
|
+
"POST",
|
|
268
|
+
messagesRoute(target),
|
|
269
|
+
{
|
|
270
|
+
content: chunk,
|
|
271
|
+
allowed_mentions: DISCORD_ALLOWED_MENTIONS,
|
|
272
|
+
...(components.length > 0 && index === chunks.length - 1
|
|
273
|
+
? { components }
|
|
274
|
+
: {}),
|
|
275
|
+
},
|
|
276
|
+
);
|
|
277
|
+
lastMessageId = typeof sent?.id === "string" ? sent.id : undefined;
|
|
278
|
+
} catch (err) {
|
|
279
|
+
// Nothing posted yet propagates plainly; a caller may retry or fall
|
|
280
|
+
// back with the full text. Past the first chunk the delivered prefix
|
|
281
|
+
// cannot be unsent, so the error names what is still owed.
|
|
282
|
+
if (index === 0) {
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
throw new DiscordPartialSendError(
|
|
286
|
+
err,
|
|
287
|
+
index,
|
|
288
|
+
chunks.slice(index).join("\n"),
|
|
289
|
+
lastMessageId,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
143
292
|
}
|
|
144
293
|
|
|
145
294
|
log.debug(
|
|
@@ -268,6 +417,44 @@ export async function sendDiscordAttachments(
|
|
|
268
417
|
* Discord expires it after about ten seconds, so a caller that wants it to
|
|
269
418
|
* persist re-sends on a timer rather than clearing it; there is no stop route.
|
|
270
419
|
*/
|
|
420
|
+
/**
|
|
421
|
+
* The REST path spelling of a reaction emoji: a custom emoji arrives in its
|
|
422
|
+
* mention form (`<:name:id>` / `<a:name:id>`, the shape inbound forwards)
|
|
423
|
+
* and the route wants the bare `name:id`; a unicode emoji rides URL-encoded
|
|
424
|
+
* as itself.
|
|
425
|
+
*/
|
|
426
|
+
function discordReactionPathEmoji(emoji: string): string {
|
|
427
|
+
const custom = /^<a?:([^:>]+):(\d+)>$/.exec(emoji);
|
|
428
|
+
return encodeURIComponent(custom ? `${custom[1]}:${custom[2]}` : emoji);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Add or remove the bot's own emoji reaction on a message.
|
|
433
|
+
*
|
|
434
|
+
* `PUT`/`DELETE .../reactions/{emoji}/@me` are idempotent (204 either way
|
|
435
|
+
* when the end state already holds), so no already-reacted special case
|
|
436
|
+
* exists. Guild channels need the `Add Reactions` permission for `PUT`;
|
|
437
|
+
* a rejection reports `ok: false` rather than throwing.
|
|
438
|
+
*/
|
|
439
|
+
export async function sendDiscordReaction(
|
|
440
|
+
chatId: string,
|
|
441
|
+
emoji: string,
|
|
442
|
+
messageId: string,
|
|
443
|
+
action: "add" | "remove",
|
|
444
|
+
): Promise<ChannelDeliveryResult> {
|
|
445
|
+
const route = `/channels/${chatId}/messages/${messageId}/reactions/${discordReactionPathEmoji(emoji)}/@me`;
|
|
446
|
+
try {
|
|
447
|
+
await callDiscordApi(action === "add" ? "PUT" : "DELETE", route);
|
|
448
|
+
return { ok: true };
|
|
449
|
+
} catch (err) {
|
|
450
|
+
log.warn(
|
|
451
|
+
{ err, chatId, messageId, action },
|
|
452
|
+
"Failed to deliver Discord reaction",
|
|
453
|
+
);
|
|
454
|
+
return { ok: false };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
271
458
|
export async function sendDiscordTypingIndicator(
|
|
272
459
|
target: DiscordSendTarget,
|
|
273
460
|
): Promise<void> {
|
|
@@ -11,6 +11,7 @@ import type { DiscordSendTarget } from "./send.js";
|
|
|
11
11
|
import {
|
|
12
12
|
editDiscordMessage,
|
|
13
13
|
sendDiscordAttachments,
|
|
14
|
+
sendDiscordReaction,
|
|
14
15
|
sendDiscordReply,
|
|
15
16
|
sendDiscordTypingIndicator,
|
|
16
17
|
} from "./send.js";
|
|
@@ -76,6 +77,15 @@ export const discordTransport: ChannelTransport = {
|
|
|
76
77
|
return { ok: true };
|
|
77
78
|
},
|
|
78
79
|
|
|
80
|
+
async react(target) {
|
|
81
|
+
return sendDiscordReaction(
|
|
82
|
+
target.threadId ?? target.chatId,
|
|
83
|
+
target.emoji,
|
|
84
|
+
target.messageId,
|
|
85
|
+
target.action,
|
|
86
|
+
);
|
|
87
|
+
},
|
|
88
|
+
|
|
79
89
|
async deliver(ctx, payload) {
|
|
80
90
|
const { chatId, text, attachments, approval } = payload;
|
|
81
91
|
const target = await sendTarget(ctx, chatId);
|
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
CallbackContext,
|
|
23
23
|
ChannelTransport,
|
|
24
24
|
EditTarget,
|
|
25
|
+
ReactionTarget,
|
|
25
26
|
} from "./channel-transport.js";
|
|
26
27
|
import { discordTransport } from "./discord/transport.js";
|
|
27
28
|
import { slackTransport } from "./slack/transport.js";
|
|
@@ -51,6 +52,47 @@ export function getTransportForCallback(
|
|
|
51
52
|
return channel ? TRANSPORTS[channel] : undefined;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Resolve a channel's transport by its id, or `undefined` for a channel with
|
|
57
|
+
* no direct transport. A plugin channel resolves to `undefined` until plugin
|
|
58
|
+
* outbound exists, so capability probes over this read as "not yet" rather
|
|
59
|
+
* than throwing on an unknown id.
|
|
60
|
+
*/
|
|
61
|
+
export function getTransportForChannel(
|
|
62
|
+
channel: string | undefined,
|
|
63
|
+
): ChannelTransport | undefined {
|
|
64
|
+
return channel && channel in TRANSPORTS
|
|
65
|
+
? TRANSPORTS[channel as DirectDeliveryChannel]
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether this channel can add or remove the assistant's own emoji
|
|
71
|
+
* reactions. Asks the transport rather than the channel id, so a channel
|
|
72
|
+
* that gains the method starts being offered without a caller being told.
|
|
73
|
+
*/
|
|
74
|
+
export function supportsChannelReaction(channel: string | undefined): boolean {
|
|
75
|
+
return getTransportForChannel(channel)?.react !== undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Add or remove one of the assistant's own emoji reactions on a message.
|
|
80
|
+
*
|
|
81
|
+
* Resolves to nothing when the channel cannot react; the tool surface gates
|
|
82
|
+
* on `supportsChannelReaction`, so reaching this without the capability is a
|
|
83
|
+
* caller that skipped the gate, not a user-visible failure.
|
|
84
|
+
*/
|
|
85
|
+
export async function sendChannelReaction(
|
|
86
|
+
channel: string,
|
|
87
|
+
target: ReactionTarget,
|
|
88
|
+
): Promise<ChannelDeliveryResult> {
|
|
89
|
+
const transport = getTransportForChannel(channel);
|
|
90
|
+
if (!transport?.react) {
|
|
91
|
+
return { ok: true };
|
|
92
|
+
}
|
|
93
|
+
return transport.react(target);
|
|
94
|
+
}
|
|
95
|
+
|
|
54
96
|
/**
|
|
55
97
|
* Whether the channel this callback addresses can show how busy the assistant
|
|
56
98
|
* is.
|
|
@@ -17,7 +17,14 @@ mock.module("../../../../oauth/connection-resolver.js", () => ({
|
|
|
17
17
|
throw new Error("OAuth fallback was not expected");
|
|
18
18
|
},
|
|
19
19
|
}));
|
|
20
|
+
// Spread rather than list: a partial factory silently drops whatever the
|
|
21
|
+
// module under test imports next, and `resolveSlackAuth` reaching
|
|
22
|
+
// `getConnectionByProvider` is what surfaced that here as a real query
|
|
23
|
+
// against a database this suite never built.
|
|
24
|
+
const actualOauthStore = await import("../../../../oauth/oauth-store.js");
|
|
20
25
|
mock.module("../../../../oauth/oauth-store.js", () => ({
|
|
26
|
+
...actualOauthStore,
|
|
27
|
+
getConnectionByProvider: () => undefined,
|
|
21
28
|
isProviderConnected: async () => false,
|
|
22
29
|
}));
|
|
23
30
|
|
|
@@ -38,6 +45,7 @@ import {
|
|
|
38
45
|
const originalFetch = globalThis.fetch;
|
|
39
46
|
let userInfoCalls: string[] = [];
|
|
40
47
|
let channelInfoCalls: string[] = [];
|
|
48
|
+
let botsInfoCalls: string[] = [];
|
|
41
49
|
|
|
42
50
|
function installFetchStub() {
|
|
43
51
|
globalThis.fetch = (async (
|
|
@@ -158,6 +166,29 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
|
|
|
158
166
|
};
|
|
159
167
|
}
|
|
160
168
|
|
|
169
|
+
if (parsed.searchParams.get("channel") === "C_FLAKY_BOT_HISTORY") {
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
has_more: false,
|
|
173
|
+
messages: [
|
|
174
|
+
{
|
|
175
|
+
type: "message",
|
|
176
|
+
subtype: "bot_message",
|
|
177
|
+
ts: "1700000004.000100",
|
|
178
|
+
bot_id: "B_FLAKY",
|
|
179
|
+
text: "first alert",
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
type: "message",
|
|
183
|
+
subtype: "bot_message",
|
|
184
|
+
ts: "1700000004.000200",
|
|
185
|
+
bot_id: "B_FLAKY",
|
|
186
|
+
text: "second alert",
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
161
192
|
if (parsed.searchParams.get("channel") === "C_BOT_HISTORY") {
|
|
162
193
|
return {
|
|
163
194
|
ok: true,
|
|
@@ -168,6 +199,39 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
|
|
|
168
199
|
subtype: "bot_message",
|
|
169
200
|
ts: "1700000003.000400",
|
|
170
201
|
bot_id: "B_ASSISTANT",
|
|
202
|
+
text: "",
|
|
203
|
+
attachments: [
|
|
204
|
+
{
|
|
205
|
+
fallback: "notify-web failure",
|
|
206
|
+
pretext: "notify-web failure by <@ULEO>",
|
|
207
|
+
title: "CI Main Web",
|
|
208
|
+
title_link: "https://example.com/actions/runs/1",
|
|
209
|
+
},
|
|
210
|
+
],
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
type: "message",
|
|
214
|
+
subtype: "bot_message",
|
|
215
|
+
ts: "1700000003.000500",
|
|
216
|
+
bot_id: "B_WEBHOOK",
|
|
217
|
+
username: "Deploy Webhook",
|
|
218
|
+
text: "",
|
|
219
|
+
blocks: [
|
|
220
|
+
{
|
|
221
|
+
type: "section",
|
|
222
|
+
text: { type: "mrkdwn", text: "Deploy finished" },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
type: "context",
|
|
226
|
+
elements: [{ type: "mrkdwn", text: "took 42s" }],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
type: "message",
|
|
232
|
+
subtype: "bot_message",
|
|
233
|
+
ts: "1700000003.000600",
|
|
234
|
+
bot_id: "B_ASSISTANT",
|
|
171
235
|
text: "Bot-authored history",
|
|
172
236
|
},
|
|
173
237
|
],
|
|
@@ -236,6 +300,20 @@ function fakeSlackResponse(url: string): Record<string, unknown> {
|
|
|
236
300
|
return fakeUserInfoResponse(userId);
|
|
237
301
|
}
|
|
238
302
|
|
|
303
|
+
if (method === "bots.info") {
|
|
304
|
+
const botId = parsed.searchParams.get("bot") ?? "";
|
|
305
|
+
botsInfoCalls.push(botId);
|
|
306
|
+
if (botId === "B_ASSISTANT") {
|
|
307
|
+
return { ok: true, bot: { id: "B_ASSISTANT", name: "CI Notifier" } };
|
|
308
|
+
}
|
|
309
|
+
// An unmapped transient failure: thrown immediately (no in-transport
|
|
310
|
+
// retry, unlike rate limits) and evicted from the cross-batch cache.
|
|
311
|
+
if (botId === "B_FLAKY") {
|
|
312
|
+
return { ok: false, error: "fatal_error" };
|
|
313
|
+
}
|
|
314
|
+
return { ok: false, error: "bot_not_found" };
|
|
315
|
+
}
|
|
316
|
+
|
|
239
317
|
return { ok: true };
|
|
240
318
|
}
|
|
241
319
|
|
|
@@ -364,6 +442,7 @@ describe("Slack adapter mention rendering", () => {
|
|
|
364
442
|
__resetSlackMentionCachesForTests();
|
|
365
443
|
userInfoCalls = [];
|
|
366
444
|
channelInfoCalls = [];
|
|
445
|
+
botsInfoCalls = [];
|
|
367
446
|
getSecureKeyAsyncMock.mockReset();
|
|
368
447
|
getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
|
|
369
448
|
if (key === credentialKey("slack_channel", "bot_token")) {
|
|
@@ -392,15 +471,62 @@ describe("Slack adapter mention rendering", () => {
|
|
|
392
471
|
expect(messages[0].reactions).toEqual([{ name: "eyes", count: 1 }]);
|
|
393
472
|
});
|
|
394
473
|
|
|
395
|
-
test("getHistory
|
|
474
|
+
test("getHistory resolves bot sender names and attachment/block content for bot-authored Slack messages", async () => {
|
|
396
475
|
const messages = await slackProvider.getHistory(undefined, "C_BOT_HISTORY");
|
|
397
476
|
|
|
398
|
-
expect(messages).toHaveLength(
|
|
399
|
-
|
|
477
|
+
expect(messages).toHaveLength(3);
|
|
478
|
+
|
|
479
|
+
// bot_id-only row: name via bots.info, content from the attachment
|
|
480
|
+
// (mention tokens inside it render like ordinary message text).
|
|
481
|
+
expect(messages[0].sender).toEqual({
|
|
482
|
+
id: "B_ASSISTANT",
|
|
483
|
+
name: "CI Notifier",
|
|
484
|
+
});
|
|
485
|
+
expect(messages[0].text).toBe(
|
|
486
|
+
"notify-web failure by @Leo\nCI Main Web (https://example.com/actions/runs/1)",
|
|
487
|
+
);
|
|
400
488
|
expect(messages[0].metadata).toEqual({
|
|
401
489
|
isBot: true,
|
|
402
490
|
slackBotId: "B_ASSISTANT",
|
|
403
491
|
});
|
|
492
|
+
|
|
493
|
+
// username-bearing row: named without a bots.info round trip, content
|
|
494
|
+
// from Block Kit blocks.
|
|
495
|
+
expect(messages[1].sender).toEqual({
|
|
496
|
+
id: "B_WEBHOOK",
|
|
497
|
+
name: "Deploy Webhook",
|
|
498
|
+
});
|
|
499
|
+
expect(messages[1].text).toBe("Deploy finished\ntook 42s");
|
|
500
|
+
expect(botsInfoCalls).not.toContain("B_WEBHOOK");
|
|
501
|
+
|
|
502
|
+
// Plain text row from the same bot: bots.info was resolved once, cached.
|
|
503
|
+
expect(messages[2].sender).toEqual({
|
|
504
|
+
id: "B_ASSISTANT",
|
|
505
|
+
name: "CI Notifier",
|
|
506
|
+
});
|
|
507
|
+
expect(messages[2].text).toBe("Bot-authored history");
|
|
508
|
+
expect(
|
|
509
|
+
botsInfoCalls.filter((botId) => botId === "B_ASSISTANT"),
|
|
510
|
+
).toHaveLength(1);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("a transient bots.info failure is attempted once per batch and falls back to the bot id", async () => {
|
|
514
|
+
const messages = await slackProvider.getHistory(
|
|
515
|
+
undefined,
|
|
516
|
+
"C_FLAKY_BOT_HISTORY",
|
|
517
|
+
);
|
|
518
|
+
|
|
519
|
+
expect(messages).toHaveLength(2);
|
|
520
|
+
expect(messages.map((message) => message.sender)).toEqual([
|
|
521
|
+
{ id: "B_FLAKY", name: "B_FLAKY" },
|
|
522
|
+
{ id: "B_FLAKY", name: "B_FLAKY" },
|
|
523
|
+
]);
|
|
524
|
+
// The transient failure is evicted from the cross-batch cache (so a later
|
|
525
|
+
// page retries), but within one page the batch memo pins the first
|
|
526
|
+
// attempt: two rows from the same bot cost one bots.info call.
|
|
527
|
+
expect(botsInfoCalls.filter((botId) => botId === "B_FLAKY")).toHaveLength(
|
|
528
|
+
1,
|
|
529
|
+
);
|
|
404
530
|
});
|
|
405
531
|
|
|
406
532
|
test("getHistory caches Slack user info and maps timezone metadata", async () => {
|
|
@@ -55,6 +55,41 @@ describe("resolveSlackAuth", () => {
|
|
|
55
55
|
test("user intent falls back to the bot token without a user token", async () => {
|
|
56
56
|
secureKeys.set(BOT_KEY, "xoxb-bot");
|
|
57
57
|
expect(await resolveSlackAuth("user")).toBe("xoxb-bot");
|
|
58
|
+
// No connection row, so the OAuth rung is skipped rather than probed.
|
|
59
|
+
expect(resolveOAuthCalls).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("user intent takes the OAuth connection over the bot token", async () => {
|
|
63
|
+
// Both are the installer's own token. Degrading to the bot here would
|
|
64
|
+
// drop `search.messages`, which a bot token cannot call, while a usable
|
|
65
|
+
// user token sat in the connection.
|
|
66
|
+
secureKeys.set(BOT_KEY, "xoxb-bot");
|
|
67
|
+
connectionByProvider["slack"] = { id: "conn-1" };
|
|
68
|
+
oauthConnection = { accessToken: "xoxp-oauth" };
|
|
69
|
+
|
|
70
|
+
expect((await resolveSlackAuth("user")) as unknown).toBe(oauthConnection);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("the pasted user token still wins over an OAuth connection", async () => {
|
|
74
|
+
secureKeys.set(BOT_KEY, "xoxb-bot");
|
|
75
|
+
secureKeys.set(USER_KEY, "xoxp-user");
|
|
76
|
+
connectionByProvider["slack"] = { id: "conn-1" };
|
|
77
|
+
oauthConnection = { accessToken: "xoxp-oauth" };
|
|
78
|
+
|
|
79
|
+
expect(await resolveSlackAuth("user")).toBe("xoxp-user");
|
|
80
|
+
// Resolved from the credential store, so the connection is never touched.
|
|
81
|
+
expect(resolveOAuthCalls).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("an unresolvable OAuth connection degrades to the bot token", async () => {
|
|
85
|
+
// A revoked or unrefreshable connection must not take messaging down: the
|
|
86
|
+
// bot token still serves every read it can.
|
|
87
|
+
secureKeys.set(BOT_KEY, "xoxb-bot");
|
|
88
|
+
connectionByProvider["slack"] = { id: "conn-1" };
|
|
89
|
+
oauthConnection = null; // the mock throws for this
|
|
90
|
+
|
|
91
|
+
expect(await resolveSlackAuth("user")).toBe("xoxb-bot");
|
|
92
|
+
expect(resolveOAuthCalls).toHaveLength(1);
|
|
58
93
|
});
|
|
59
94
|
|
|
60
95
|
test("resolves the refreshing OAuth connection for legacy installs", async () => {
|