@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,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the guardian-form registry.
|
|
3
|
+
*
|
|
4
|
+
* The registry is the rail every form-backed command parks on, so the
|
|
5
|
+
* concurrency rules are tested here directly rather than only through the
|
|
6
|
+
* contact forms that happen to use them today. What matters: a form is
|
|
7
|
+
* answered once, a claim stops the answer deadline without ending the form,
|
|
8
|
+
* and a form that nobody answers ends by telling the clients showing it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
claimGuardianForm,
|
|
15
|
+
type GuardianFormClosedReason,
|
|
16
|
+
hasUnclaimedGuardianForm,
|
|
17
|
+
openGuardianForm,
|
|
18
|
+
resolveGuardianForm,
|
|
19
|
+
} from "../guardian-form-registry.js";
|
|
20
|
+
|
|
21
|
+
interface TestResult {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
wrote?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Open a form and hand back its requestId plus the parked promise. */
|
|
28
|
+
function openForm(options: { kind?: string; timeoutMs?: number } = {}) {
|
|
29
|
+
const closed = mock((_id: string, _reason: GuardianFormClosedReason) => {});
|
|
30
|
+
let requestId = "";
|
|
31
|
+
const settled = openGuardianForm<TestResult>({
|
|
32
|
+
kind: options.kind ?? "test.form",
|
|
33
|
+
timeoutMs: options.timeoutMs ?? 30_000,
|
|
34
|
+
meta: { verify: true },
|
|
35
|
+
broadcast: {
|
|
36
|
+
open: (id) => {
|
|
37
|
+
requestId = id;
|
|
38
|
+
},
|
|
39
|
+
closed,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
return { requestId, settled, closed };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tick = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
46
|
+
|
|
47
|
+
describe("openGuardianForm", () => {
|
|
48
|
+
test("broadcasts the form and parks until something resolves it", async () => {
|
|
49
|
+
const { requestId, settled } = openForm();
|
|
50
|
+
expect(requestId).toMatch(/^[0-9a-f-]{36}$/);
|
|
51
|
+
|
|
52
|
+
let done = false;
|
|
53
|
+
void settled.then(() => {
|
|
54
|
+
done = true;
|
|
55
|
+
});
|
|
56
|
+
await tick(10);
|
|
57
|
+
expect(done).toBe(false);
|
|
58
|
+
|
|
59
|
+
resolveGuardianForm(requestId, { ok: true, wrote: "contact-1" });
|
|
60
|
+
expect(await settled).toEqual({ ok: true, wrote: "contact-1" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("hands the writer's own fields back to the parked call", async () => {
|
|
64
|
+
const { requestId, settled } = openForm();
|
|
65
|
+
resolveGuardianForm(requestId, { ok: false, error: "Cancelled by user" });
|
|
66
|
+
expect(await settled).toEqual({ ok: false, error: "Cancelled by user" });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("an unanswered form ends and tells the clients showing it", async () => {
|
|
70
|
+
const { requestId, settled, closed } = openForm({ timeoutMs: 30 });
|
|
71
|
+
|
|
72
|
+
expect(await settled).toEqual({ ok: false, error: "Prompt timed out" });
|
|
73
|
+
expect(closed).toHaveBeenCalledWith(requestId, "timed_out");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("claimGuardianForm", () => {
|
|
78
|
+
test("only the first claim wins", async () => {
|
|
79
|
+
const { requestId, settled } = openForm();
|
|
80
|
+
|
|
81
|
+
const first = claimGuardianForm(requestId);
|
|
82
|
+
const second = claimGuardianForm(requestId);
|
|
83
|
+
|
|
84
|
+
expect(first.claimed).toBe(true);
|
|
85
|
+
expect(first.settleMs).toBeGreaterThan(0);
|
|
86
|
+
expect(second).toEqual({ claimed: false, reason: "already_claimed" });
|
|
87
|
+
|
|
88
|
+
resolveGuardianForm(requestId, { ok: true });
|
|
89
|
+
await settled;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("a form nobody is holding claims as unknown", () => {
|
|
93
|
+
expect(claimGuardianForm("no-such-form")).toEqual({
|
|
94
|
+
claimed: false,
|
|
95
|
+
reason: "unknown",
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("claiming disarms the answer deadline", async () => {
|
|
100
|
+
// The form is open for 30ms, and the claim lands inside that window. If the
|
|
101
|
+
// claim did not swap the deadline for the settle window, the parked call
|
|
102
|
+
// would report a timeout while the write it started was still running.
|
|
103
|
+
const { requestId, settled, closed } = openForm({ timeoutMs: 30 });
|
|
104
|
+
expect(claimGuardianForm(requestId).claimed).toBe(true);
|
|
105
|
+
|
|
106
|
+
let done = false;
|
|
107
|
+
void settled.then(() => {
|
|
108
|
+
done = true;
|
|
109
|
+
});
|
|
110
|
+
await tick(80);
|
|
111
|
+
|
|
112
|
+
expect(done).toBe(false);
|
|
113
|
+
expect(closed).not.toHaveBeenCalled();
|
|
114
|
+
|
|
115
|
+
resolveGuardianForm(requestId, { ok: true, wrote: "late" });
|
|
116
|
+
expect(await settled).toEqual({ ok: true, wrote: "late" });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a resolved form cannot be claimed again", async () => {
|
|
120
|
+
const { requestId, settled } = openForm();
|
|
121
|
+
resolveGuardianForm(requestId, { ok: true });
|
|
122
|
+
await settled;
|
|
123
|
+
|
|
124
|
+
expect(claimGuardianForm(requestId)).toEqual({
|
|
125
|
+
claimed: false,
|
|
126
|
+
reason: "unknown",
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("resolveGuardianForm", () => {
|
|
132
|
+
test("retires the card as answered or cancelled", async () => {
|
|
133
|
+
const answered = openForm();
|
|
134
|
+
resolveGuardianForm(answered.requestId, { ok: true });
|
|
135
|
+
await answered.settled;
|
|
136
|
+
expect(answered.closed).toHaveBeenCalledWith(
|
|
137
|
+
answered.requestId,
|
|
138
|
+
"answered",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const cancelled = openForm();
|
|
142
|
+
resolveGuardianForm(cancelled.requestId, { ok: false, error: "no" });
|
|
143
|
+
await cancelled.settled;
|
|
144
|
+
expect(cancelled.closed).toHaveBeenCalledWith(
|
|
145
|
+
cancelled.requestId,
|
|
146
|
+
"cancelled",
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("reports that nothing was waiting for an unknown form", () => {
|
|
151
|
+
expect(resolveGuardianForm("no-such-form", { ok: true })).toEqual({
|
|
152
|
+
resolved: false,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("hasUnclaimedGuardianForm", () => {
|
|
158
|
+
test("sees only the kinds it is asked about", async () => {
|
|
159
|
+
const { requestId, settled } = openForm({ kind: "kind.a" });
|
|
160
|
+
|
|
161
|
+
expect(hasUnclaimedGuardianForm(["kind.a"])).toBe(true);
|
|
162
|
+
// The point of taking kinds: an open form of one kind must not block a
|
|
163
|
+
// form of another, which a registry-wide sweep would do.
|
|
164
|
+
expect(hasUnclaimedGuardianForm(["kind.b"])).toBe(false);
|
|
165
|
+
expect(hasUnclaimedGuardianForm(["kind.b", "kind.a"])).toBe(true);
|
|
166
|
+
|
|
167
|
+
resolveGuardianForm(requestId, { ok: true });
|
|
168
|
+
await settled;
|
|
169
|
+
expect(hasUnclaimedGuardianForm(["kind.a"])).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a claimed form no longer counts as open", async () => {
|
|
173
|
+
const { requestId, settled } = openForm({ kind: "kind.c" });
|
|
174
|
+
expect(hasUnclaimedGuardianForm(["kind.c"])).toBe(true);
|
|
175
|
+
|
|
176
|
+
claimGuardianForm(requestId);
|
|
177
|
+
// Claimed means answered: its card is gone, so the next command should not
|
|
178
|
+
// be refused on account of it.
|
|
179
|
+
expect(hasUnclaimedGuardianForm(["kind.c"])).toBe(false);
|
|
180
|
+
|
|
181
|
+
resolveGuardianForm(requestId, { ok: true });
|
|
182
|
+
await settled;
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("resolveFormFromCallback", () => {
|
|
187
|
+
test("passes a writer's own fields through untouched", async () => {
|
|
188
|
+
// The rail is only reusable if a new form's result reaches its caller. A
|
|
189
|
+
// callback that enumerated contact fields would drop everything else.
|
|
190
|
+
const { requestId, settled } = openForm();
|
|
191
|
+
|
|
192
|
+
const { resolveFormFromCallback } =
|
|
193
|
+
await import("../routes/guardian-form-routes.js");
|
|
194
|
+
resolveFormFromCallback({
|
|
195
|
+
body: { requestId, id: "row-7", rows: 3, skipped: false },
|
|
196
|
+
} as never);
|
|
197
|
+
|
|
198
|
+
expect(await settled).toEqual({
|
|
199
|
+
ok: true,
|
|
200
|
+
id: "row-7",
|
|
201
|
+
rows: 3,
|
|
202
|
+
skipped: false,
|
|
203
|
+
} as never);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("an error makes the outcome a failure", async () => {
|
|
207
|
+
const { requestId, settled } = openForm();
|
|
208
|
+
const { resolveFormFromCallback } =
|
|
209
|
+
await import("../routes/guardian-form-routes.js");
|
|
210
|
+
resolveFormFromCallback({
|
|
211
|
+
body: { requestId, error: "nope", id: "ignored" },
|
|
212
|
+
} as never);
|
|
213
|
+
|
|
214
|
+
expect(await settled).toEqual({ ok: false, error: "nope" });
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
describe("claiming names the form", () => {
|
|
219
|
+
test("a claim for a different kind is refused", async () => {
|
|
220
|
+
// An id alone does not say which form it belongs to. Without this check a
|
|
221
|
+
// submission to form B could claim an open form A, run B's write, and hand
|
|
222
|
+
// A's caller a result for a form it never showed.
|
|
223
|
+
const { requestId, settled } = openForm({ kind: "kind.real" });
|
|
224
|
+
|
|
225
|
+
expect(claimGuardianForm(requestId, "kind.other")).toEqual({
|
|
226
|
+
claimed: false,
|
|
227
|
+
reason: "wrong_kind",
|
|
228
|
+
});
|
|
229
|
+
// Refused, not consumed: the right caller can still claim it.
|
|
230
|
+
expect(claimGuardianForm(requestId, "kind.real").claimed).toBe(true);
|
|
231
|
+
|
|
232
|
+
resolveGuardianForm(requestId, { ok: true });
|
|
233
|
+
await settled;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("a caller that names no kind still claims, as before", async () => {
|
|
237
|
+
const { requestId, settled } = openForm({ kind: "kind.real" });
|
|
238
|
+
expect(claimGuardianForm(requestId).claimed).toBe(true);
|
|
239
|
+
resolveGuardianForm(requestId, { ok: true });
|
|
240
|
+
await settled;
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* principal so access requests cannot bind to stale/cross-assistant contacts.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import { isGuardianRequestExpired } from "@vellumai/gateway-client";
|
|
15
|
+
|
|
14
16
|
import {
|
|
15
17
|
createGuardianRequest,
|
|
16
18
|
listGuardianRequestsOrEmpty,
|
|
@@ -228,13 +230,19 @@ export async function notifyGuardianOfAccessRequest(
|
|
|
228
230
|
// notified: true with the existing request ID so callers know the guardian
|
|
229
231
|
// was already notified. A degraded (empty) read falls through to creation,
|
|
230
232
|
// whose fail-closed throw prevents a prompt without a persisted request.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
233
|
+
// Past-deadline rows never dedupe: a pending row can sit past its
|
|
234
|
+
// expiresAt until the sweep expires it, it is already undecidable, and
|
|
235
|
+
// letting it absorb a fresh attempt would silently drop the new guardian
|
|
236
|
+
// notification.
|
|
237
|
+
const existingPending = (
|
|
238
|
+
await listGuardianRequestsOrEmpty({
|
|
239
|
+
status: "pending",
|
|
240
|
+
requesterExternalUserId: actorExternalId,
|
|
241
|
+
sourceChannel,
|
|
242
|
+
kind: "access_request",
|
|
243
|
+
sourceConversationId: conversationId,
|
|
244
|
+
})
|
|
245
|
+
).filter((r) => !isGuardianRequestExpired(r));
|
|
238
246
|
if (existingPending.length > 0) {
|
|
239
247
|
log.debug(
|
|
240
248
|
{ sourceChannel, actorExternalId, existingId: existingPending[0].id },
|
|
@@ -145,9 +145,9 @@ function sanitizeEventAttr(value: string): string {
|
|
|
145
145
|
/**
|
|
146
146
|
* Untrusted third-party output to fence inside a persisted wake trigger via
|
|
147
147
|
* {@link wrapUntrustedContent}. `maxChars` overrides the per-source character
|
|
148
|
-
* budget
|
|
149
|
-
*
|
|
150
|
-
*
|
|
148
|
+
* budget. Used for preformatted shell output that is already bounded to
|
|
149
|
+
* `MAX_OUTPUT_LENGTH` with an `<output_truncated />` marker, so the wrapper
|
|
150
|
+
* does not slice that marker off.
|
|
151
151
|
*/
|
|
152
152
|
interface WakeUntrustedOutput {
|
|
153
153
|
content: string;
|
|
@@ -1018,7 +1018,19 @@ export async function wakeAgentForOpportunity(
|
|
|
1018
1018
|
},
|
|
1019
1019
|
];
|
|
1020
1020
|
const wakeHintMessageCount = wakeMessages.length;
|
|
1021
|
-
|
|
1021
|
+
// Camera-frame retention, the wake's equivalent of the orchestrator's
|
|
1022
|
+
// pre-run pass. The wake sends this array itself and keeps the in-loop
|
|
1023
|
+
// budget gate disabled, so neither the loop's post-compaction transform nor
|
|
1024
|
+
// `runAgentLoopImpl`'s pre-run pass ever sees it; a wake on a long camera
|
|
1025
|
+
// session would otherwise carry every frame the conversation holds,
|
|
1026
|
+
// including any the gate above just retained. The pass replaces blocks
|
|
1027
|
+
// inside messages without adding, dropping, or reordering one, so
|
|
1028
|
+
// `baselineLength` and `wakeHintMessageCount` still index the same
|
|
1029
|
+
// positions.
|
|
1030
|
+
const runInput: Message[] = conversation.trimAgedSightFrames([
|
|
1031
|
+
...baseline,
|
|
1032
|
+
...wakeMessages,
|
|
1033
|
+
]);
|
|
1022
1034
|
|
|
1023
1035
|
// Event handling runs in two modes. While `mode === "buffering"`,
|
|
1024
1036
|
// events accumulate in `buffered` so that a wake which ultimately
|
|
@@ -160,12 +160,15 @@ export interface ChannelApprovalPrompt {
|
|
|
160
160
|
// Decision result (daemon-internal)
|
|
161
161
|
// ---------------------------------------------------------------------------
|
|
162
162
|
|
|
163
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* How the user communicated their decision: the modality, never the channel.
|
|
165
|
+
* A channel-prefixed value here would misattribute the next channel that
|
|
166
|
+
* joins (every channel's buttons ride the same `apr:` callback), and no
|
|
167
|
+
* consumer reads a channel off this field.
|
|
168
|
+
*/
|
|
164
169
|
export type ApprovalDecisionSource =
|
|
165
|
-
| "
|
|
166
|
-
| "
|
|
167
|
-
| "slack_button"
|
|
168
|
-
| "slack_reaction"
|
|
170
|
+
| "button"
|
|
171
|
+
| "reaction"
|
|
169
172
|
| "vellum_surface"
|
|
170
173
|
| "plain_text";
|
|
171
174
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { ChannelId } from "../channels/types.js";
|
|
16
|
+
import { discordInviteAdapter } from "./channel-invite-transports/discord.js";
|
|
16
17
|
import { emailInviteAdapter } from "./channel-invite-transports/email.js";
|
|
17
18
|
import { slackInviteAdapter } from "./channel-invite-transports/slack.js";
|
|
18
19
|
import { telegramInviteAdapter } from "./channel-invite-transports/telegram.js";
|
|
@@ -96,6 +97,7 @@ export async function resolveAdapterHandle(
|
|
|
96
97
|
/** Create a registry instance with built-in adapters registered. */
|
|
97
98
|
export function createInviteAdapterRegistry(): InviteAdapterRegistry {
|
|
98
99
|
const registry = new InviteAdapterRegistry();
|
|
100
|
+
registry.register(discordInviteAdapter);
|
|
99
101
|
registry.register(emailInviteAdapter);
|
|
100
102
|
registry.register(slackInviteAdapter);
|
|
101
103
|
registry.register(telegramInviteAdapter);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord channel invite adapter.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the assistant's Discord bot username for use in invite
|
|
5
|
+
* instructions. Discord invites use the universal 6-digit code path for
|
|
6
|
+
* redemption, so this adapter only implements `resolveChannelHandle`,
|
|
7
|
+
* no `buildShareLink` or `extractInboundToken` needed. The platform
|
|
8
|
+
* constraint that shapes the instruction copy lives with the copy
|
|
9
|
+
* (`invite-instruction-generator.ts`): a person can only DM the bot once
|
|
10
|
+
* they share a server with it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ChannelId } from "../../channels/types.js";
|
|
14
|
+
import { getConfig } from "../../config/loader.js";
|
|
15
|
+
import type { ChannelInviteAdapter } from "../channel-invite-types.js";
|
|
16
|
+
|
|
17
|
+
export const discordInviteAdapter: ChannelInviteAdapter = {
|
|
18
|
+
channel: "discord" as ChannelId,
|
|
19
|
+
|
|
20
|
+
resolveChannelHandle(): string | undefined {
|
|
21
|
+
const { botUsername } = getConfig().discord;
|
|
22
|
+
if (!botUsername) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return `@${botUsername}`;
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -26,10 +26,10 @@ export const REMOTE_TTL_MS = 5 * 60 * 1000;
|
|
|
26
26
|
* Bot scopes the Slack manifest requests that the app cannot work without.
|
|
27
27
|
*
|
|
28
28
|
* Slack's install flow can return a token carrying a fraction of the manifest's
|
|
29
|
-
* scopes while `auth.test` still succeeds
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* scopes while `auth.test` still succeeds, leaving the first real API call to
|
|
30
|
+
* fail with `missing_scope`. `auth.test` passing is therefore not evidence the
|
|
31
|
+
* install is usable; the granted set has to be read off the `x-oauth-scopes`
|
|
32
|
+
* response header and compared.
|
|
33
33
|
*
|
|
34
34
|
* Mirrors the non-optional half of `oauth_config.scopes.bot` in
|
|
35
35
|
* `skills/slack-app-setup/scripts/build-manifest.ts` (and its copy in
|
|
@@ -3,6 +3,7 @@ import type { MessageAudience } from "@vellumai/gateway-client";
|
|
|
3
3
|
import { stripVellumLinks } from "../daemon/assistant-attachments.js";
|
|
4
4
|
import type { RenderedHistoryContent } from "../daemon/handlers/shared.js";
|
|
5
5
|
import { renderHistoryContent } from "../daemon/handlers/shared.js";
|
|
6
|
+
import { readProviderMessageMetadata } from "../messaging/provider-message-metadata.js";
|
|
6
7
|
import { editChannelMessage } from "../messaging/providers/index.js";
|
|
7
8
|
import { readSlackMetadata } from "../messaging/providers/slack/message-metadata.js";
|
|
8
9
|
import { getAttachmentMetadataForMessage } from "../persistence/attachments-store.js";
|
|
@@ -383,16 +384,16 @@ async function deliverPersistedAssistantMessageViaCallback(
|
|
|
383
384
|
return false;
|
|
384
385
|
}
|
|
385
386
|
|
|
386
|
-
// Compose an `onMessageTs` that reconciles
|
|
387
|
-
//
|
|
388
|
-
// assistant row
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
const reconcileOnMessageTs =
|
|
387
|
+
// Compose an `onMessageTs` that reconciles the persisted assistant row's
|
|
388
|
+
// provider message ids as the transport reports the authoritative ones.
|
|
389
|
+
// The assistant row is written BEFORE the gateway POST, so its pre-send
|
|
390
|
+
// envelope names no id of its own: a Slack row's partial `slackMeta` lacks
|
|
391
|
+
// `channelTs` and reads as null through `readSlackMetadata`, and a
|
|
392
|
+
// neutral-envelope row lacks the `messageId` a later reaction
|
|
393
|
+
// naming it resolves by. A reply split into several segments reports one
|
|
394
|
+
// id per posted provider message, all reconciled onto this one row; see
|
|
395
|
+
// `makeSentMessageIdReconciler` for the per-envelope rules.
|
|
396
|
+
const reconcileOnMessageTs = makeSentMessageIdReconciler(msg.id);
|
|
396
397
|
const callerOnMessageTs = options?.onMessageTs;
|
|
397
398
|
const composedOnMessageTs = (ts: string): void => {
|
|
398
399
|
reconcileOnMessageTs(ts);
|
|
@@ -491,39 +492,40 @@ export async function deliverReplyViaCallback(
|
|
|
491
492
|
}
|
|
492
493
|
|
|
493
494
|
/**
|
|
494
|
-
* Build
|
|
495
|
-
*
|
|
495
|
+
* Build an `onMessageTs` handler that reconciles the persisted assistant
|
|
496
|
+
* row's provider message ids from the transport's authoritative ones:
|
|
497
|
+
* `slackMeta.channelTs` for a Slack row, `providerMeta.messageId` plus
|
|
498
|
+
* `additionalMessageIds` for a row carrying the neutral envelope (Discord
|
|
499
|
+
* today, any transport whose delivery result reports the sent id). The
|
|
500
|
+
* back-filled ids are what let a later reaction naming the
|
|
501
|
+
* assistant's own post resolve back to this row.
|
|
496
502
|
*
|
|
497
503
|
* Behavior:
|
|
498
|
-
* -
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
* -
|
|
502
|
-
*
|
|
503
|
-
*
|
|
504
|
-
*
|
|
505
|
-
*
|
|
504
|
+
* - A Slack row acts on the first invocation only: `channelTs` is the one
|
|
505
|
+
* id its envelope names, and later split segments are independent Slack
|
|
506
|
+
* messages it cannot carry.
|
|
507
|
+
* - A neutral-envelope row records every reported id: the first as
|
|
508
|
+
* `messageId`, the rest under `additionalMessageIds`, all naming this
|
|
509
|
+
* same row. An id already recorded (a redelivery) is skipped.
|
|
510
|
+
* - No-op when the row was persisted with neither envelope (e.g. vellum
|
|
511
|
+
* outbound).
|
|
506
512
|
* - Failures are logged and swallowed so a transient DB error cannot break
|
|
507
513
|
* the outbound delivery itself.
|
|
508
514
|
*/
|
|
509
|
-
function
|
|
510
|
-
let
|
|
515
|
+
function makeSentMessageIdReconciler(messageId: string): (ts: string) => void {
|
|
516
|
+
let slackApplied = false;
|
|
511
517
|
return (ts: string): void => {
|
|
512
|
-
if (applied) {
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
applied = true;
|
|
516
518
|
if (!ts) {
|
|
517
519
|
return;
|
|
518
520
|
}
|
|
519
521
|
try {
|
|
520
522
|
// Re-read the row's current metadata so a concurrent edit-propagation
|
|
521
523
|
// write (e.g. `editedAt`) is not clobbered. `updateMessageMetadata`
|
|
522
|
-
// shallow-merges into the top-level envelope, and the
|
|
524
|
+
// shallow-merges into the top-level envelope, and the per-channel
|
|
523
525
|
// sub-object is merged manually below so we can preserve fields on
|
|
524
526
|
// the partial pre-send envelope (`mergeSlackMetadata` would call
|
|
525
527
|
// `readSlackMetadata` which rejects the partial form for lacking
|
|
526
|
-
// channelTs
|
|
528
|
+
// channelTs, exactly the state we are reconciling).
|
|
527
529
|
const row = getMessageById(messageId);
|
|
528
530
|
if (row === null || row.metadata === null) {
|
|
529
531
|
return;
|
|
@@ -537,8 +539,13 @@ function makeChannelTsReconciler(messageId: string): (ts: string) => void {
|
|
|
537
539
|
const slackMetaRaw =
|
|
538
540
|
typeof envelope.slackMeta === "string" ? envelope.slackMeta : null;
|
|
539
541
|
if (slackMetaRaw === null) {
|
|
542
|
+
reconcileProviderMessageId(messageId, envelope, ts);
|
|
540
543
|
return;
|
|
541
544
|
}
|
|
545
|
+
if (slackApplied) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
slackApplied = true;
|
|
542
549
|
// If the existing slackMeta already parses cleanly via the strict
|
|
543
550
|
// reader, channelTs is already present (a prior reconciliation ran,
|
|
544
551
|
// or backfill stamped the field) — nothing to do.
|
|
@@ -579,3 +586,40 @@ function makeChannelTsReconciler(messageId: string): (ts: string) => void {
|
|
|
579
586
|
}
|
|
580
587
|
};
|
|
581
588
|
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* The neutral-envelope arm of the reconciliation: record the transport's
|
|
592
|
+
* sent-message id on the row's `providerMeta`. The first reported id
|
|
593
|
+
* becomes `messageId`; further ids (a reply split at tool boundaries posts
|
|
594
|
+
* several provider messages) accumulate under `additionalMessageIds`. An id
|
|
595
|
+
* the envelope already names is a redelivery and is skipped, and a row
|
|
596
|
+
* carrying no valid neutral envelope is left untouched.
|
|
597
|
+
*/
|
|
598
|
+
function reconcileProviderMessageId(
|
|
599
|
+
messageId: string,
|
|
600
|
+
envelope: Record<string, unknown>,
|
|
601
|
+
ts: string,
|
|
602
|
+
): void {
|
|
603
|
+
const providerMeta = readProviderMessageMetadata(envelope.providerMeta);
|
|
604
|
+
if (providerMeta === null) {
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (providerMeta.messageId === undefined) {
|
|
608
|
+
updateMessageMetadata(messageId, {
|
|
609
|
+
providerMeta: JSON.stringify({ ...providerMeta, messageId: ts }),
|
|
610
|
+
});
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (
|
|
614
|
+
providerMeta.messageId === ts ||
|
|
615
|
+
providerMeta.additionalMessageIds?.includes(ts)
|
|
616
|
+
) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
updateMessageMetadata(messageId, {
|
|
620
|
+
providerMeta: JSON.stringify({
|
|
621
|
+
...providerMeta,
|
|
622
|
+
additionalMessageIds: [...(providerMeta.additionalMessageIds ?? []), ts],
|
|
623
|
+
}),
|
|
624
|
+
});
|
|
625
|
+
}
|
|
@@ -101,6 +101,10 @@ export async function processGuardianDecision(
|
|
|
101
101
|
// 2. Verify conversationId scoping before applying the decision. The
|
|
102
102
|
// decision is allowed when the conversationId matches the request's
|
|
103
103
|
// source conversation OR a recorded delivery destination conversation.
|
|
104
|
+
// Deliberately not narrowed by delivery channel: a delivery row's
|
|
105
|
+
// destinationConversationId is always an internal conversation id, and
|
|
106
|
+
// a channel card's paired conversation renders the same actionable
|
|
107
|
+
// in-app card, so its taps are as legitimate as the vellum card's.
|
|
104
108
|
// Reads degrade fail-closed: an unreachable gateway scopes to not_found
|
|
105
109
|
// (and the decide below fails loudly anyway).
|
|
106
110
|
if (conversationId) {
|
|
@@ -108,11 +112,7 @@ export async function processGuardianDecision(
|
|
|
108
112
|
if (
|
|
109
113
|
request &&
|
|
110
114
|
request.sourceConversationId &&
|
|
111
|
-
!(await isGuardianRequestInScopeOrFalse(
|
|
112
|
-
requestId,
|
|
113
|
-
conversationId,
|
|
114
|
-
channel,
|
|
115
|
-
))
|
|
115
|
+
!(await isGuardianRequestInScopeOrFalse(requestId, conversationId))
|
|
116
116
|
) {
|
|
117
117
|
return { ok: true, applied: false, reason: "not_found" };
|
|
118
118
|
}
|
|
@@ -128,6 +128,10 @@ export async function processGuardianDecision(
|
|
|
128
128
|
channel,
|
|
129
129
|
guardianPrincipalId: actorContext.guardianPrincipalId,
|
|
130
130
|
},
|
|
131
|
+
// The acting conversation: card withdrawal suppresses only this
|
|
132
|
+
// conversation's completion broadcast (the acting client already shows
|
|
133
|
+
// its optimistic completion); sibling in-app projections still get theirs.
|
|
134
|
+
...(conversationId ? { originConversationId: conversationId } : {}),
|
|
131
135
|
userText: undefined,
|
|
132
136
|
});
|
|
133
137
|
|