@vellumai/assistant 0.11.4 → 0.11.5-staging.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +2 -2
- package/ARCHITECTURE.md +29 -4
- package/README.md +1 -1
- package/docs/architecture/memory.md +17 -5
- package/docs/architecture/security.md +96 -152
- package/docs/guardian-request-flow.md +13 -2
- 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/__tests__/url-normalization.test.ts +135 -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/url-normalization.ts +107 -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/__tests__/url-normalization.test.ts +135 -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/url-normalization.ts +107 -0
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +166 -1
- package/node_modules/@vellumai/gateway-client/src/http-delivery.ts +7 -14
- package/node_modules/@vellumai/gateway-client/src/index.ts +2 -0
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +29 -31
- package/node_modules/@vellumai/service-contracts/package.json +1 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/url-normalization.test.ts +135 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/url-normalization.ts +107 -0
- package/openapi.yaml +193 -3
- package/package.json +1 -1
- package/src/__tests__/anthropic-provider.test.ts +10 -3
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +1 -1
- package/src/__tests__/channel-approval-routes.test.ts +4 -0
- package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -6
- package/src/__tests__/channel-reply-delivery.test.ts +26 -13
- package/src/__tests__/checker.test.ts +20 -314
- package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +6 -2
- package/src/__tests__/client-os-metadata-persistence.test.ts +16 -0
- package/src/__tests__/compactor-retained-image-validation.test.ts +142 -0
- package/src/__tests__/config-loader-backfill.test.ts +19 -10
- package/src/__tests__/container-cpu-sampler.test.ts +153 -0
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/conversation-attachments.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +17 -0
- package/src/__tests__/conversation-lifecycle.test.ts +20 -26
- package/src/__tests__/conversation-pairing.test.ts +186 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +21 -2
- package/src/__tests__/credential-broker-server-use.test.ts +24 -5
- package/src/__tests__/credential-routes.test.ts +22 -3
- package/src/__tests__/credential-security-invariants.test.ts +2 -1
- package/src/__tests__/daemon-credential-client.test.ts +88 -0
- package/src/__tests__/default-plugin-names-guard.test.ts +12 -1
- package/src/__tests__/dm-backfill.test.ts +63 -0
- package/src/__tests__/dynamic-skill-background-guard.test.ts +0 -2
- package/src/__tests__/edit-propagation.test.ts +107 -4
- package/src/__tests__/events-client-registration.test.ts +28 -0
- package/src/__tests__/file-write-tool.test.ts +4 -2
- package/src/__tests__/guardian-prompt-notice-privacy.test.ts +133 -0
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +143 -97
- package/src/__tests__/helpers/gateway-classify-mock.ts +27 -6
- package/src/__tests__/host-proxy-interface.test.ts +11 -1
- package/src/__tests__/host-shell-tool.test.ts +23 -4
- package/src/__tests__/image-conversion.test.ts +143 -1
- package/src/__tests__/inline-skill-load-permissions.test.ts +47 -36
- package/src/__tests__/input-repairs.test.ts +207 -0
- package/src/__tests__/live-workspace-guard.test.ts +75 -0
- package/src/__tests__/managed-profile-guard.test.ts +4 -2
- package/src/__tests__/mcp-abort-signal.test.ts +1 -1
- package/src/__tests__/mcp-client-auth.test.ts +1 -1
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +1 -1
- package/src/__tests__/media-resolve-image-validation.test.ts +310 -0
- package/src/__tests__/mtime-cache.test.ts +2 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +124 -1
- package/src/__tests__/openai-provider.test.ts +22 -0
- package/src/__tests__/personal-memory-auth-bypass.test.ts +22 -6
- package/src/__tests__/platform-bash-auto-approve.test.ts +0 -4
- package/src/__tests__/platform.test.ts +16 -1
- package/src/__tests__/plugin-api-resolve-credential.test.ts +22 -13
- package/src/__tests__/plugin-api-shim.test.ts +5 -0
- package/src/__tests__/plugin-api-store-credential.test.ts +268 -0
- package/src/__tests__/plugin-config-data-migration.test.ts +8 -1
- package/src/__tests__/plugin-disabled-state.test.ts +2 -0
- package/src/__tests__/plugin-effective-enabled-set.test.ts +55 -3
- package/src/__tests__/plugin-execution-context.test.ts +73 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -1
- package/src/__tests__/reaction-persistence.test.ts +200 -7
- package/src/__tests__/require-fresh-approval.test.ts +0 -4
- package/src/__tests__/resource-pressure-guard.test.ts +428 -0
- package/src/__tests__/resource-pressure-routes.test.ts +113 -0
- package/src/__tests__/risk-classification-boundary-guard.test.ts +115 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +27 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +7 -0
- package/src/__tests__/skill-tool-factory.test.ts +161 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +132 -5
- package/src/__tests__/tool-executor.test.ts +63 -38
- package/src/__tests__/tool-policy.test.ts +97 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +7 -4
- package/src/__tests__/user-plugin-loader.test.ts +2 -0
- package/src/__tests__/validate-input.test.ts +243 -7
- package/src/__tests__/verification-control-plane-policy.test.ts +0 -2
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +2 -0
- package/src/__tests__/voice-session-bridge.test.ts +42 -0
- package/src/acp/__tests__/acp-claude-oauth.test.ts +97 -29
- package/src/acp/__tests__/prepare-agent-env.test.ts +376 -153
- package/src/acp/acp-claude-oauth.ts +38 -21
- package/src/acp/prepare-agent-env.ts +146 -55
- package/src/agent/loop-tool-dedup.test.ts +161 -0
- package/src/agent/loop.ts +68 -22
- package/src/api/constants/app-tools.ts +34 -0
- package/src/api/events/resource-pressure-status-changed.ts +53 -0
- package/src/api/index.ts +19 -0
- package/src/api/responses/resource-pressure-status.ts +25 -0
- package/src/approvals/guardian-channel-delivery.ts +70 -6
- package/src/approvals/guardian-request-resolvers.ts +32 -66
- package/src/calls/__tests__/voice-session-bridge.test.ts +114 -0
- package/src/calls/voice-session-bridge.ts +60 -6
- package/src/channels/__tests__/message-audience.test.ts +49 -0
- package/src/channels/__tests__/types.test.ts +17 -3
- package/src/channels/message-audience.ts +40 -0
- package/src/channels/types.ts +16 -12
- package/src/cli/__tests__/catalog-search-help.test.ts +50 -0
- package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +31 -0
- package/src/cli/commands/__tests__/conversations-search.test.ts +289 -0
- package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
- package/src/cli/commands/__tests__/inference-providers.test.ts +68 -0
- package/src/cli/commands/__tests__/keys.test.ts +89 -8
- package/src/cli/commands/__tests__/plugins.test.ts +57 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +11 -9
- package/src/cli/commands/channel-verification-sessions.ts +11 -21
- package/src/cli/commands/conversations.help.ts +34 -0
- package/src/cli/commands/conversations.ts +125 -0
- package/src/cli/commands/inference-providers.ts +9 -4
- package/src/cli/commands/inference.help.ts +9 -4
- package/src/cli/commands/keys.help.ts +13 -1
- package/src/cli/commands/keys.ts +26 -15
- package/src/cli/commands/memory/__tests__/memory-v2.test.ts +57 -7
- package/src/cli/commands/memory/index.help.ts +41 -27
- package/src/cli/commands/memory/index.ts +2 -0
- package/src/cli/commands/memory/memory-v2.ts +58 -54
- package/src/cli/commands/memory/memory-validate.ts +18 -0
- package/src/cli/commands/monitoring.ts +1 -0
- package/src/cli/commands/plugins.help.ts +28 -31
- package/src/cli/commands/plugins.ts +10 -0
- package/src/cli/commands/skills.help.ts +13 -16
- package/src/cli/commands/trust.ts +3 -14
- package/src/cli/lib/__tests__/install-from-github.test.ts +38 -0
- package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +34 -0
- package/src/cli/lib/__tests__/plugin-surfaces.test.ts +32 -1
- package/src/cli/lib/__tests__/toggle-plugin.test.ts +2 -1
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +64 -0
- package/src/cli/lib/bundled-marketplace.json +13 -0
- package/src/cli/lib/daemon-credential-client.ts +25 -3
- package/src/cli/lib/install-from-github.ts +35 -24
- package/src/cli/lib/merge-plugin-tree.ts +22 -4
- package/src/cli/lib/plugin-surfaces.ts +27 -0
- package/src/config/__tests__/balanced-model-experiment.test.ts +7 -7
- package/src/config/__tests__/default-profile-catalog.test.ts +3 -3
- package/src/config/default-profile-catalog.ts +5 -6
- package/src/config/feature-flag-registry.json +32 -32
- package/src/config/webhook-routing.ts +8 -0
- package/src/context/compactor.ts +31 -2
- package/src/daemon/__tests__/provider-rejection-log-fields.test.ts +272 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +4 -1
- package/src/daemon/conversation-error.ts +13 -0
- package/src/daemon/conversation-messaging.ts +22 -2
- package/src/daemon/conversation-process.ts +11 -0
- package/src/daemon/conversation-store.ts +10 -0
- package/src/daemon/conversation-surfaces.ts +21 -8
- package/src/daemon/conversation.ts +12 -10
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/message-types/conversations.ts +5 -7
- package/src/daemon/provider-rejection-log-fields.ts +124 -0
- package/src/daemon/resource-pressure-guard-lifecycle.ts +66 -0
- package/src/daemon/resource-pressure-guard.ts +387 -0
- package/src/daemon/shutdown-handlers.ts +2 -0
- package/src/daemon/startup-error.ts +16 -0
- package/src/daemon/trust-context.ts +14 -10
- package/src/daemon/unsendable-image-notice.ts +76 -0
- package/src/hooks/registry.ts +65 -51
- package/src/ipc/__tests__/socket-path.test.ts +15 -7
- package/src/ipc/gateway-client.test.ts +1 -1
- package/src/ipc/gateway-client.ts +14 -21
- package/src/ipc/socket-cleanup.ts +2 -11
- package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -1
- package/src/messaging/provider-message-metadata.ts +128 -0
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +116 -68
- package/src/messaging/providers/channel-transport.ts +90 -13
- package/src/messaging/providers/discord/send.ts +16 -0
- package/src/messaging/providers/discord/transport.ts +11 -1
- package/src/messaging/providers/index.ts +109 -16
- package/src/messaging/providers/slack/message-metadata.ts +45 -0
- package/src/messaging/providers/slack/render-transcript.ts +1 -4
- package/src/messaging/providers/slack/send.test.ts +6 -9
- package/src/messaging/providers/slack/send.ts +40 -28
- package/src/messaging/providers/slack/transport.ts +22 -26
- package/src/messaging/providers/telegram-bot/send.test.ts +2 -8
- package/src/messaging/providers/telegram-bot/transport.ts +3 -6
- package/src/messaging/read-provider-metadata.test.ts +157 -0
- package/src/messaging/read-provider-metadata.ts +51 -0
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +94 -0
- package/src/notifications/__tests__/edit-notification.test.ts +174 -2
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +536 -4
- package/src/notifications/adapters/macos.ts +55 -0
- package/src/notifications/adapters/slack.ts +10 -5
- package/src/notifications/assistant-reply-producer.ts +37 -4
- package/src/notifications/conversation-pairing.ts +111 -21
- package/src/notifications/edit-notification.ts +38 -8
- package/src/notifications/emit-signal.ts +12 -14
- package/src/notifications/home-feed-side-effect.ts +218 -15
- package/src/notifications/types.ts +5 -0
- package/src/permissions/AGENTS.md +16 -0
- package/src/permissions/checker.test.ts +75 -122
- package/src/permissions/checker.ts +44 -560
- package/src/permissions/confirmation-guardian-request.test.ts +28 -3
- package/src/permissions/confirmation-guardian-request.ts +5 -1
- package/src/persistence/conversation-crud.ts +23 -20
- package/src/persistence/conversation-queries.ts +14 -1
- package/src/persistence/conversation-types.ts +22 -0
- package/src/persistence/delivery-crud.ts +122 -15
- package/src/plugin-api/__tests__/import-graph-partial-mock.test.ts +67 -0
- package/src/plugin-api/conversation-turn.ts +27 -0
- package/src/plugin-api/credential-scope.test.ts +15 -0
- package/src/plugin-api/credential-scope.ts +14 -0
- package/src/plugin-api/index.ts +19 -1
- package/src/plugin-api/resolve-credential.ts +15 -10
- package/src/plugin-api/store-credential.ts +148 -0
- package/src/plugin-api/system-card.ts +36 -0
- package/src/plugin-api/vision-support.test.ts +82 -0
- package/src/plugin-api/vision-support.ts +14 -2
- package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +361 -110
- package/src/plugins/defaults/image-fallback/__tests__/vision-recovery.test.ts +4 -0
- package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +58 -0
- package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +64 -23
- package/src/plugins/defaults/image-recovery/detect.ts +24 -1
- package/src/plugins/defaults/main.ts +15 -0
- package/src/plugins/defaults/memory/AGENTS.md +11 -3
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +0 -1
- package/src/plugins/defaults/memory/graph-topology/pending-buffer.ts +9 -12
- package/src/plugins/defaults/memory/memory-retrospective-prompt.ts +1 -1
- package/src/plugins/defaults/memory/src/memory-v2-routes.ts +12 -10
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +114 -0
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +8 -0
- package/src/plugins/defaults/memory/substrate/__tests__/edge-index.test.ts +0 -30
- package/src/plugins/defaults/memory/substrate/__tests__/ingest.test.ts +73 -1
- package/src/plugins/defaults/memory/substrate/__tests__/page-index.test.ts +37 -0
- package/src/plugins/defaults/memory/substrate/__tests__/page-links.test.ts +208 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +158 -1
- package/src/plugins/defaults/memory/substrate/__tests__/static-context.test.ts +1 -61
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +68 -6
- package/src/plugins/defaults/memory/substrate/edge-index.ts +0 -31
- package/src/plugins/defaults/memory/substrate/ingest.ts +59 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +35 -8
- package/src/plugins/defaults/memory/substrate/page-links.ts +133 -0
- package/src/plugins/defaults/memory/substrate/page-store.ts +10 -0
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +124 -34
- package/src/plugins/defaults/memory/substrate/static-context.ts +0 -29
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +3 -1
- package/src/plugins/defaults/memory/v3/card.ts +9 -9
- package/src/plugins/defaults/memory/v3/core-set.test.ts +7 -0
- package/src/plugins/defaults/memory/v3/core-set.ts +5 -1
- package/src/plugins/defaults/memory/v3/edge.ts +13 -57
- package/src/plugins/mtime-cache.ts +1 -1
- package/src/plugins/pipeline.ts +14 -7
- package/src/plugins/plugin-execution-context.ts +35 -11
- package/src/plugins/plugin-tree-walk.ts +63 -4
- package/src/providers/anthropic/__tests__/pause-turn-continuation.test.ts +170 -0
- package/src/providers/anthropic/client.ts +332 -241
- package/src/providers/connection-resolution.ts +2 -1
- package/src/providers/content-blocks.ts +22 -0
- package/src/providers/gemini/client.ts +5 -2
- package/src/providers/inference/__tests__/adapter-factory-litellm.test.ts +47 -1
- package/src/providers/inference/__tests__/adapter-factory-ollama.test.ts +83 -0
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +98 -1
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +38 -0
- package/src/providers/inference/__tests__/base-url-security.test.ts +10 -0
- package/src/providers/inference/__tests__/connections-ollama.test.ts +90 -0
- package/src/providers/inference/__tests__/missing-credential-guard.test.ts +117 -0
- package/src/providers/inference/adapter-factory.ts +33 -2
- package/src/providers/inference/auth.ts +13 -2
- package/src/providers/inference/credential-usage.ts +37 -0
- package/src/providers/inference/missing-credential-guard.ts +110 -0
- package/src/providers/inference/resolve-auth.ts +7 -7
- package/src/providers/media-resolve.ts +176 -15
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +576 -46
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +125 -2
- package/src/providers/openai/chat-completions-provider.ts +322 -42
- package/src/routes/route-host-protocol.ts +7 -0
- package/src/routes/worker.ts +31 -10
- package/src/runtime/AGENTS.md +35 -0
- package/src/runtime/__tests__/runtime-http-port-collision.test.ts +131 -0
- package/src/runtime/__tests__/web-presence.test.ts +234 -0
- package/src/runtime/assistant-event-hub.ts +38 -0
- package/src/runtime/channel-reply-delivery.ts +54 -31
- package/src/runtime/channel-retry-sweep.ts +6 -4
- package/src/runtime/effective-capabilities.test.ts +19 -0
- package/src/runtime/effective-capabilities.ts +25 -0
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/http-errors.ts +1 -0
- package/src/runtime/http-server.ts +20 -5
- package/src/runtime/routes/__tests__/client-routes.test.ts +150 -0
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +133 -0
- package/src/runtime/routes/__tests__/credential-delete-in-use.test.ts +155 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +52 -1
- package/src/runtime/routes/__tests__/monitoring-routes.test.ts +1 -0
- package/src/runtime/routes/__tests__/pending-interactions-route.test.ts +175 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +88 -35
- package/src/runtime/routes/__tests__/surface-action-routes.test.ts +55 -6
- package/src/runtime/routes/__tests__/system-card-turn-termination.test.ts +107 -0
- package/src/runtime/routes/__tests__/user-route-dispatcher-host.test.ts +52 -1
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +75 -4
- package/src/runtime/routes/approval-routes.ts +37 -4
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +16 -22
- package/src/runtime/routes/canned-message-complete.ts +68 -29
- package/src/runtime/routes/client-routes.ts +113 -0
- package/src/runtime/routes/conversation-list-routes.ts +22 -7
- package/src/runtime/routes/conversation-management-routes.ts +15 -9
- package/src/runtime/routes/conversation-routes.ts +36 -22
- package/src/runtime/routes/credential-in-use.ts +85 -0
- package/src/runtime/routes/credential-routes.ts +55 -85
- package/src/runtime/routes/guardian-approval-interception.ts +22 -22
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +14 -12
- package/src/runtime/routes/identity-routes.ts +5 -121
- package/src/runtime/routes/inbound-message-handler.ts +71 -27
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -17
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +77 -50
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +61 -97
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +101 -77
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +17 -8
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +91 -7
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +72 -70
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +9 -7
- package/src/runtime/routes/monitoring-routes.ts +9 -1
- package/src/runtime/routes/plugins-routes.ts +17 -5
- package/src/runtime/routes/resource-pressure-routes.ts +22 -0
- package/src/runtime/routes/secret-routes.ts +39 -8
- package/src/runtime/routes/settings-routes.ts +6 -6
- package/src/runtime/routes/surface-action-routes.ts +20 -19
- package/src/runtime/routes/user-route-dispatcher.ts +31 -2
- package/src/runtime/routes/user-route-resolution.ts +18 -1
- package/src/runtime/slack-reply-session.test.ts +23 -13
- package/src/runtime/slack-reply-session.ts +38 -55
- package/src/runtime/web-presence.ts +90 -0
- package/src/skills/validate-input.ts +177 -40
- package/src/subagent/__tests__/consult-context-gating.test.ts +6 -10
- package/src/subagent/consult-context.ts +7 -23
- package/src/tools/credentials/broker.ts +24 -79
- package/src/tools/credentials/ref-parse.ts +35 -0
- package/src/tools/credentials/resolve.ts +4 -10
- package/src/tools/credentials/store.ts +168 -0
- package/src/tools/credentials/tool-policy.ts +67 -0
- package/src/tools/executor.ts +53 -15
- package/src/tools/network/__tests__/web-search.test.ts +41 -1
- package/src/tools/network/url-safety.ts +9 -16
- package/src/tools/network/web-search.ts +21 -3
- package/src/tools/permission-checker.ts +43 -52
- package/src/tools/schema-transforms.ts +40 -5
- package/src/tools/shared/input-repairs.ts +160 -0
- package/src/tools/skills/skill-tool-factory.ts +18 -4
- package/src/tools/subagent/spawn.ts +0 -1
- package/src/tools/tool-approval-handler.ts +16 -10
- package/src/tools/tool-types.ts +26 -13
- package/src/tools/types.ts +6 -6
- package/src/util/__tests__/cgroup-memory.test.ts +3 -0
- package/src/util/cgroup-memory.ts +3 -0
- package/src/util/container-cpu-sampler.ts +250 -0
- package/src/util/image-conversion.ts +178 -14
- package/src/util/platform.ts +93 -10
- package/src/permissions/ipc-risk-types.ts +0 -143
- package/src/permissions/risk-types.ts +0 -76
|
@@ -37,7 +37,6 @@ import {
|
|
|
37
37
|
import {
|
|
38
38
|
check,
|
|
39
39
|
classifyRisk,
|
|
40
|
-
generateAllowlistOptions,
|
|
41
40
|
generateScopeOptions,
|
|
42
41
|
} from "../../permissions/checker.js";
|
|
43
42
|
import { resolveGuardianPersonaPath } from "../../prompts/persona-resolver.js";
|
|
@@ -710,19 +709,22 @@ async function handleToolPermissionSimulate({ body = {} }: RouteHandlerArgs) {
|
|
|
710
709
|
isInteractive === false ? "headless" : "conversation";
|
|
711
710
|
const policyContext = { executionTarget, executionContext } as const;
|
|
712
711
|
|
|
713
|
-
const
|
|
712
|
+
const classification = await classifyRisk(
|
|
714
713
|
toolName,
|
|
715
714
|
input,
|
|
716
715
|
workingDir,
|
|
717
716
|
undefined,
|
|
718
717
|
manifestOverride,
|
|
719
718
|
);
|
|
719
|
+
const riskLevel = classification.level;
|
|
720
720
|
const result = await check(
|
|
721
721
|
toolName,
|
|
722
722
|
input,
|
|
723
723
|
workingDir,
|
|
724
724
|
policyContext,
|
|
725
725
|
manifestOverride,
|
|
726
|
+
undefined,
|
|
727
|
+
classification,
|
|
726
728
|
);
|
|
727
729
|
|
|
728
730
|
if (isInteractive === false && result.decision === "prompt") {
|
|
@@ -743,11 +745,9 @@ async function handleToolPermissionSimulate({ body = {} }: RouteHandlerArgs) {
|
|
|
743
745
|
| undefined;
|
|
744
746
|
|
|
745
747
|
if (result.decision === "prompt") {
|
|
746
|
-
const allowlistOptions = await generateAllowlistOptions(toolName, input);
|
|
747
|
-
const scopeOptions = generateScopeOptions(workingDir, toolName);
|
|
748
748
|
promptPayload = {
|
|
749
|
-
allowlistOptions,
|
|
750
|
-
scopeOptions,
|
|
749
|
+
allowlistOptions: classification.allowlistOptions ?? [],
|
|
750
|
+
scopeOptions: generateScopeOptions(workingDir, toolName),
|
|
751
751
|
persistentDecisionsAllowed: true,
|
|
752
752
|
};
|
|
753
753
|
}
|
|
@@ -37,26 +37,25 @@ const log = getLogger("surface-action-routes");
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Resolve trust context for the actor principal from the gateway guardian
|
|
40
|
-
* binding and
|
|
41
|
-
* nobody, so the mapper yields guardian or
|
|
42
|
-
* dev-bypass principal translation and the
|
|
43
|
-
*
|
|
44
|
-
*
|
|
40
|
+
* binding, and return it for the caller to attribute this action with. A vellum
|
|
41
|
+
* principal is the guardian or nobody, so the mapper yields guardian or
|
|
42
|
+
* unknown. Resolution (including the dev-bypass principal translation and the
|
|
43
|
+
* mutating reset-drift repair) lives in the shared
|
|
44
|
+
* {@link resolveVellumActorTrustContext} so the surface content route's
|
|
45
|
+
* read-only requester scoping cannot drift from it.
|
|
46
|
+
*
|
|
47
|
+
* Resolution only: the requester travels to `handleSurfaceAction` as an
|
|
48
|
+
* argument, and the conversation's slot is stamped there, at the point the
|
|
49
|
+
* click is committed to starting a turn. Stamping here instead would repoint
|
|
50
|
+
* the actor of a turn already in flight, or of a turn that starts between this
|
|
51
|
+
* call and the dispatch.
|
|
45
52
|
*/
|
|
46
|
-
async function
|
|
47
|
-
conversation: {
|
|
48
|
-
setTrustContext?(ctx: TrustContext): void;
|
|
49
|
-
},
|
|
53
|
+
async function resolveRequesterTrustContext(
|
|
50
54
|
actorPrincipalId: string | undefined,
|
|
51
|
-
): Promise<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
conversation.setTrustContext(
|
|
56
|
-
await resolveVellumActorTrustContext(actorPrincipalId, {
|
|
57
|
-
healResetDrift: true,
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
55
|
+
): Promise<TrustContext> {
|
|
56
|
+
return resolveVellumActorTrustContext(actorPrincipalId, {
|
|
57
|
+
healResetDrift: true,
|
|
58
|
+
});
|
|
60
59
|
}
|
|
61
60
|
|
|
62
61
|
// ---------------------------------------------------------------------------
|
|
@@ -154,7 +153,8 @@ async function handleSurfaceAction({
|
|
|
154
153
|
}
|
|
155
154
|
|
|
156
155
|
const actorPrincipalId = headers?.["x-vellum-actor-principal-id"];
|
|
157
|
-
|
|
156
|
+
const requesterTrustContext =
|
|
157
|
+
await resolveRequesterTrustContext(actorPrincipalId);
|
|
158
158
|
|
|
159
159
|
// Translate dev-bypass → real guardian so the surface turn's principal matches
|
|
160
160
|
// the SSE host-proxy client's registered principal; otherwise CU/app-control
|
|
@@ -170,6 +170,7 @@ async function handleSurfaceAction({
|
|
|
170
170
|
actionId,
|
|
171
171
|
data,
|
|
172
172
|
resolvedActorPrincipalId,
|
|
173
|
+
requesterTrustContext,
|
|
173
174
|
);
|
|
174
175
|
const result =
|
|
175
176
|
raw && typeof raw === "object" && "accepted" in raw
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
|
|
38
38
|
import { statSync } from "node:fs";
|
|
39
39
|
|
|
40
|
+
import { runInPluginContext } from "../../plugins/plugin-execution-context.js";
|
|
40
41
|
import { isRouteHostEnabled } from "../../routes/control.js";
|
|
41
42
|
import {
|
|
42
43
|
RouteHostClient,
|
|
@@ -55,6 +56,7 @@ import {
|
|
|
55
56
|
routeSourceRoot,
|
|
56
57
|
} from "./user-route-import.js";
|
|
57
58
|
import {
|
|
59
|
+
pluginNameForRoutePath,
|
|
58
60
|
resolveHandlerFile,
|
|
59
61
|
resolveRouteLocation,
|
|
60
62
|
} from "./user-route-resolution.js";
|
|
@@ -150,11 +152,36 @@ export class UserRouteDispatcher {
|
|
|
150
152
|
);
|
|
151
153
|
}
|
|
152
154
|
|
|
155
|
+
// A plugin's own routes execute as that plugin, so the host APIs a handler
|
|
156
|
+
// reaches scope to it exactly as they do inside its hooks and tools. Both
|
|
157
|
+
// execution paths carry it: in-thread here, and the route host through the
|
|
158
|
+
// invoke params it marks its own execution with.
|
|
159
|
+
const pluginName = pluginNameForRoutePath(routePath);
|
|
160
|
+
|
|
153
161
|
if (isRouteHostEnabled()) {
|
|
154
|
-
return this.dispatchViaHost(filePath, routePath, request);
|
|
162
|
+
return this.dispatchViaHost(filePath, routePath, request, pluginName);
|
|
155
163
|
}
|
|
156
164
|
|
|
157
|
-
|
|
165
|
+
// The context covers module evaluation, not just the handler call. A route
|
|
166
|
+
// module can reach a scoped host API at import time (a top-level `await`),
|
|
167
|
+
// and that import happens once per mtime, so a load left outside the
|
|
168
|
+
// context would take the unscoped branch and never be re-run scoped.
|
|
169
|
+
const serve = () =>
|
|
170
|
+
this.serveInThread(filePath, location.routesDir, routePath, request);
|
|
171
|
+
return pluginName ? runInPluginContext(pluginName, serve) : serve();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Load the handler module and run the method's handler in this thread.
|
|
176
|
+
* Called inside the owning plugin's execution context, when the route has one.
|
|
177
|
+
*/
|
|
178
|
+
private async serveInThread(
|
|
179
|
+
filePath: string,
|
|
180
|
+
routesDir: string,
|
|
181
|
+
routePath: string,
|
|
182
|
+
request: Request,
|
|
183
|
+
): Promise<Response> {
|
|
184
|
+
const mod = await this.loadModule(filePath, routesDir);
|
|
158
185
|
const method = request.method as HttpMethod;
|
|
159
186
|
const handler = mod.handlers[method];
|
|
160
187
|
|
|
@@ -180,6 +207,7 @@ export class UserRouteDispatcher {
|
|
|
180
207
|
filePath: string,
|
|
181
208
|
routePath: string,
|
|
182
209
|
request: Request,
|
|
210
|
+
pluginName: string | undefined,
|
|
183
211
|
): Promise<Response> {
|
|
184
212
|
const mtimeMs = statSync(filePath).mtimeMs;
|
|
185
213
|
|
|
@@ -201,6 +229,7 @@ export class UserRouteDispatcher {
|
|
|
201
229
|
method: request.method,
|
|
202
230
|
url: request.url,
|
|
203
231
|
headers,
|
|
232
|
+
pluginName,
|
|
204
233
|
},
|
|
205
234
|
body,
|
|
206
235
|
);
|
|
@@ -66,6 +66,23 @@ export interface RouteLocation {
|
|
|
66
66
|
subPath: string;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The manifest name of the plugin whose namespace an `/x/` route path falls in,
|
|
71
|
+
* or undefined for a workspace route.
|
|
72
|
+
*
|
|
73
|
+
* The dispatcher marks that plugin as in context for the handler's execution,
|
|
74
|
+
* so host APIs a route handler reaches (`resolveCredential`,
|
|
75
|
+
* `indexDocument`) scope to the owning plugin exactly as they do inside its
|
|
76
|
+
* hooks and tools.
|
|
77
|
+
*/
|
|
78
|
+
export function pluginNameForRoutePath(routePath: string): string | undefined {
|
|
79
|
+
const segments = routePath.split("/");
|
|
80
|
+
if (segments[0] !== PLUGIN_ROUTE_SEGMENT) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return segments[1] || undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
/**
|
|
70
87
|
* Resolve an `/x/` route path to the base directory + sub-path the handler file
|
|
71
88
|
* is looked up under.
|
|
@@ -87,7 +104,7 @@ export interface RouteLocation {
|
|
|
87
104
|
export function resolveRouteLocation(routePath: string): RouteLocation | null {
|
|
88
105
|
const segments = routePath.split("/");
|
|
89
106
|
if (segments[0] === PLUGIN_ROUTE_SEGMENT) {
|
|
90
|
-
const pluginName =
|
|
107
|
+
const pluginName = pluginNameForRoutePath(routePath);
|
|
91
108
|
if (!pluginName) {
|
|
92
109
|
return null;
|
|
93
110
|
}
|
|
@@ -14,6 +14,22 @@ let deliverImpl: (
|
|
|
14
14
|
ts: "stream-ts-1",
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
+
const sentStreamOps: Array<Record<string, unknown>> = [];
|
|
18
|
+
let streamOpImpl: (op: Record<string, unknown>) => Promise<{
|
|
19
|
+
ok: boolean;
|
|
20
|
+
ts?: string;
|
|
21
|
+
}> = async () => ({ ok: true, ts: "stream-ts-1" });
|
|
22
|
+
mock.module("../messaging/providers/index.js", () => ({
|
|
23
|
+
sendChannelStreamOp: async (
|
|
24
|
+
_callbackUrl: string,
|
|
25
|
+
_chatId: string,
|
|
26
|
+
op: Record<string, unknown>,
|
|
27
|
+
) => {
|
|
28
|
+
sentStreamOps.push(op);
|
|
29
|
+
return streamOpImpl(op);
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
17
33
|
mock.module("./gateway-client.js", () => ({
|
|
18
34
|
deliverChannelReply: async (
|
|
19
35
|
callbackUrl: string,
|
|
@@ -96,10 +112,7 @@ const taskProgressUpdate = (
|
|
|
96
112
|
const tick = (ms: number): Promise<void> =>
|
|
97
113
|
new Promise((resolve) => setTimeout(resolve, ms));
|
|
98
114
|
|
|
99
|
-
const slackStreamOps = (): Array<Record<string, unknown>> =>
|
|
100
|
-
deliverCalls
|
|
101
|
-
.map((call) => call.payload.slackStream as Record<string, unknown>)
|
|
102
|
-
.filter(Boolean);
|
|
115
|
+
const slackStreamOps = (): Array<Record<string, unknown>> => sentStreamOps;
|
|
103
116
|
|
|
104
117
|
const streamedMarkdown = (): string =>
|
|
105
118
|
slackStreamOps()
|
|
@@ -108,6 +121,8 @@ const streamedMarkdown = (): string =>
|
|
|
108
121
|
|
|
109
122
|
beforeEach(() => {
|
|
110
123
|
deliverCalls.length = 0;
|
|
124
|
+
sentStreamOps.length = 0;
|
|
125
|
+
streamOpImpl = async () => ({ ok: true, ts: "stream-ts-1" });
|
|
111
126
|
deliverImpl = async () => ({ ok: true, ts: "stream-ts-1" });
|
|
112
127
|
});
|
|
113
128
|
|
|
@@ -314,7 +329,7 @@ describe("createSlackReplySession", () => {
|
|
|
314
329
|
});
|
|
315
330
|
|
|
316
331
|
test("falls back when startStream returns no stream ts", async () => {
|
|
317
|
-
|
|
332
|
+
streamOpImpl = async () => ({ ok: false });
|
|
318
333
|
const session = createSlackReplySession({
|
|
319
334
|
sourceChannel: "slack",
|
|
320
335
|
chatType: "im",
|
|
@@ -331,7 +346,7 @@ describe("createSlackReplySession", () => {
|
|
|
331
346
|
});
|
|
332
347
|
|
|
333
348
|
test("falls back when startStream throws", async () => {
|
|
334
|
-
|
|
349
|
+
streamOpImpl = async () => {
|
|
335
350
|
throw new Error("rate limited");
|
|
336
351
|
};
|
|
337
352
|
const session = createSlackReplySession({
|
|
@@ -349,8 +364,7 @@ describe("createSlackReplySession", () => {
|
|
|
349
364
|
});
|
|
350
365
|
|
|
351
366
|
test("falls back when stopStream throws after streaming text", async () => {
|
|
352
|
-
|
|
353
|
-
const op = payload.slackStream as { action: string };
|
|
367
|
+
streamOpImpl = async (op) => {
|
|
354
368
|
if (op.action === "stop") {
|
|
355
369
|
throw new Error("stop failed");
|
|
356
370
|
}
|
|
@@ -482,11 +496,7 @@ describe("createSlackReplySession", () => {
|
|
|
482
496
|
});
|
|
483
497
|
|
|
484
498
|
test("leaves progress to stop when the task-only append fails", async () => {
|
|
485
|
-
|
|
486
|
-
const op = payload.slackStream as {
|
|
487
|
-
action: string;
|
|
488
|
-
markdownText?: string;
|
|
489
|
-
};
|
|
499
|
+
streamOpImpl = async (op) => {
|
|
490
500
|
if (op.action === "append" && op.markdownText === undefined) {
|
|
491
501
|
throw new Error("chunks-only append rejected");
|
|
492
502
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SlackStreamTask } from "@vellumai/gateway-client";
|
|
1
|
+
import type { SlackStreamOp, SlackStreamTask } from "@vellumai/gateway-client";
|
|
2
2
|
|
|
3
3
|
import type { AssistantEvent } from "../api/index.js";
|
|
4
4
|
import {
|
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
incompleteVellumLinkSuffixLength,
|
|
11
11
|
stripVellumLinks,
|
|
12
12
|
} from "../daemon/assistant-attachments.js";
|
|
13
|
+
import { sendChannelStreamOp } from "../messaging/providers/index.js";
|
|
13
14
|
import { SLACK_STREAM_MARKDOWN_LIMIT } from "../messaging/providers/slack/api.js";
|
|
14
15
|
import { renderSlackBlocks } from "../messaging/providers/slack/render.js";
|
|
15
16
|
import { getLogger } from "../util/logger.js";
|
|
16
17
|
import { needsBoundarySpace } from "../util/text-spacing.js";
|
|
17
|
-
import { deliverChannelReply } from "./gateway-client.js";
|
|
18
18
|
import {
|
|
19
19
|
hasDeliverableAssistantText,
|
|
20
20
|
NO_RESPONSE_INLINE_RE,
|
|
@@ -113,7 +113,6 @@ export function createSlackReplySession(params: {
|
|
|
113
113
|
chatType?: string;
|
|
114
114
|
replyCallbackUrl?: string;
|
|
115
115
|
chatId: string;
|
|
116
|
-
assistantId?: string;
|
|
117
116
|
/** Slack user ID of the reader; required to stream into a channel. */
|
|
118
117
|
recipientUserId?: string;
|
|
119
118
|
/** Slack team ID of the reader; required to stream into a channel. */
|
|
@@ -132,7 +131,7 @@ export function createSlackReplySession(params: {
|
|
|
132
131
|
if (!shouldStreamSlackReply(params) || !params.replyCallbackUrl) {
|
|
133
132
|
return undefined;
|
|
134
133
|
}
|
|
135
|
-
const { chatId,
|
|
134
|
+
const { chatId, recipientUserId, recipientTeamId } = params;
|
|
136
135
|
const coalesceMs = params.coalesceMs ?? STREAM_COALESCE_MS;
|
|
137
136
|
const replyCallbackUrl = params.replyCallbackUrl;
|
|
138
137
|
const threadTs = extractThreadTsFromCallbackUrl(replyCallbackUrl);
|
|
@@ -197,11 +196,11 @@ export function createSlackReplySession(params: {
|
|
|
197
196
|
): string | undefined =>
|
|
198
197
|
tasks ? JSON.stringify({ title, tasks }) : undefined;
|
|
199
198
|
|
|
199
|
+
// Typed from the stream operation these are passed to, so the element type
|
|
200
|
+
// follows that contract rather than drifting from it.
|
|
200
201
|
const imageBlocks = (
|
|
201
202
|
text: string,
|
|
202
|
-
):
|
|
203
|
-
| NonNullable<Parameters<typeof deliverChannelReply>[1]["blocks"]>
|
|
204
|
-
| undefined => {
|
|
203
|
+
): Extract<SlackStreamOp, { action: "stop" }>["blocks"] => {
|
|
205
204
|
const blocks = renderSlackBlocks(text)?.filter(
|
|
206
205
|
(block) => block.type === "image",
|
|
207
206
|
);
|
|
@@ -222,24 +221,20 @@ export function createSlackReplySession(params: {
|
|
|
222
221
|
const title = activeProgress?.title;
|
|
223
222
|
const tasks = planTasks();
|
|
224
223
|
try {
|
|
225
|
-
const result = await
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
...(tasks ? { tasks } : {}),
|
|
240
|
-
...(recipientUserId ? { recipientUserId } : {}),
|
|
241
|
-
...(recipientTeamId ? { recipientTeamId } : {}),
|
|
242
|
-
},
|
|
224
|
+
const result = await sendChannelStreamOp(replyCallbackUrl, chatId, {
|
|
225
|
+
action: "start",
|
|
226
|
+
threadTs,
|
|
227
|
+
markdownText: firstChunk,
|
|
228
|
+
// The task display mode is fixed for the stream's lifetime at
|
|
229
|
+
// start, while a `task_progress` surface usually appears only
|
|
230
|
+
// after the first text flush has opened the stream. Plan mode
|
|
231
|
+
// only affects how task chunks render, so a stream that never
|
|
232
|
+
// carries tasks still reads as a plain message.
|
|
233
|
+
taskDisplayMode: "plan" as const,
|
|
234
|
+
...(title ? { planTitle: title } : {}),
|
|
235
|
+
...(tasks ? { tasks } : {}),
|
|
236
|
+
...(recipientUserId ? { recipientUserId } : {}),
|
|
237
|
+
...(recipientTeamId ? { recipientTeamId } : {}),
|
|
243
238
|
});
|
|
244
239
|
if (result.ok && result.ts) {
|
|
245
240
|
streamTs = result.ts;
|
|
@@ -287,16 +282,12 @@ export function createSlackReplySession(params: {
|
|
|
287
282
|
confirmedLength + SLACK_STREAM_MARKDOWN_LIMIT,
|
|
288
283
|
);
|
|
289
284
|
try {
|
|
290
|
-
await
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
markdownText: chunk,
|
|
297
|
-
...(title ? { planTitle: title } : {}),
|
|
298
|
-
...(tasks ? { tasks } : {}),
|
|
299
|
-
},
|
|
285
|
+
await sendChannelStreamOp(replyCallbackUrl, chatId, {
|
|
286
|
+
action: "append",
|
|
287
|
+
streamTs,
|
|
288
|
+
markdownText: chunk,
|
|
289
|
+
...(title ? { planTitle: title } : {}),
|
|
290
|
+
...(tasks ? { tasks } : {}),
|
|
300
291
|
});
|
|
301
292
|
confirmedLength += chunk.length;
|
|
302
293
|
deliveredProgressKey = key ?? deliveredProgressKey;
|
|
@@ -316,15 +307,11 @@ export function createSlackReplySession(params: {
|
|
|
316
307
|
// next text append and `stopStream` carries the final state.
|
|
317
308
|
if (!taskOnlyAppendsDisabled && tasks && key !== deliveredProgressKey) {
|
|
318
309
|
try {
|
|
319
|
-
await
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
streamTs,
|
|
325
|
-
...(title ? { planTitle: title } : {}),
|
|
326
|
-
tasks,
|
|
327
|
-
},
|
|
310
|
+
await sendChannelStreamOp(replyCallbackUrl, chatId, {
|
|
311
|
+
action: "append",
|
|
312
|
+
streamTs,
|
|
313
|
+
...(title ? { planTitle: title } : {}),
|
|
314
|
+
tasks,
|
|
328
315
|
});
|
|
329
316
|
deliveredProgressKey = key;
|
|
330
317
|
} catch (err) {
|
|
@@ -450,17 +437,13 @@ export function createSlackReplySession(params: {
|
|
|
450
437
|
const title = activeProgress?.title;
|
|
451
438
|
const tasks = planTasks();
|
|
452
439
|
try {
|
|
453
|
-
await
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
...(blocks ? { blocks } : {}),
|
|
461
|
-
...(title ? { planTitle: title } : {}),
|
|
462
|
-
...(tasks ? { tasks } : {}),
|
|
463
|
-
},
|
|
440
|
+
await sendChannelStreamOp(replyCallbackUrl, chatId, {
|
|
441
|
+
action: "stop",
|
|
442
|
+
streamTs,
|
|
443
|
+
...(remaining.length > 0 ? { markdownText: remaining } : {}),
|
|
444
|
+
...(blocks ? { blocks } : {}),
|
|
445
|
+
...(title ? { planTitle: title } : {}),
|
|
446
|
+
...(tasks ? { tasks } : {}),
|
|
464
447
|
});
|
|
465
448
|
confirmedLength = clean.length;
|
|
466
449
|
} catch (err) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web presence policy.
|
|
3
|
+
*
|
|
4
|
+
* Answers exactly one question for notification producers: is the user
|
|
5
|
+
* demonstrably looking at this specific conversation in a web browser tab
|
|
6
|
+
* right now? Producers use the answer to skip an APNs push the user would
|
|
7
|
+
* see on screen anyway.
|
|
8
|
+
*
|
|
9
|
+
* Fails open by design, the same posture as desktop presence
|
|
10
|
+
* (`runtime/desktop-presence.ts`): presence is in-memory, best-effort, and
|
|
11
|
+
* reported by a client that can drop off (crash, network loss, backgrounded
|
|
12
|
+
* tab killed by the OS) without telling the daemon, so anything short of a
|
|
13
|
+
* fresh, visible, matching-conversation report answers `false` and the push
|
|
14
|
+
* goes out. A missed notification is strictly worse than a redundant one.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately separate from desktop presence: a web report is scoped to one
|
|
17
|
+
* conversation (the tab's focused thread), not the whole app. The Electron
|
|
18
|
+
* desktop renderer detects its own host OS and tags its turns
|
|
19
|
+
* `macos`/`windows` (`detectClientOs()` in the web client), so an Electron
|
|
20
|
+
* turn is already covered by `isDesktopAttended()` and never reaches this
|
|
21
|
+
* gate. Do not fold this into `setClientPresence` / the `macos`-scoped
|
|
22
|
+
* desktop-attendance path.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { getLogger } from "../util/logger.js";
|
|
26
|
+
import { assistantEventHub } from "./assistant-event-hub.js";
|
|
27
|
+
|
|
28
|
+
const log = getLogger("web-presence");
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Shared semantic + transport freshness bound. The semantic report says what
|
|
32
|
+
* the tab was showing; `lastActiveAt` says the owning SSE connection is still
|
|
33
|
+
* alive. Both must be fresh so a live-but-hidden stream cannot preserve an old
|
|
34
|
+
* visible report, and a fresh report cannot suppress after its transport dies.
|
|
35
|
+
*/
|
|
36
|
+
export const WEB_PRESENCE_STALE_AFTER_MS = 180_000;
|
|
37
|
+
|
|
38
|
+
export interface WebPresenceOptions {
|
|
39
|
+
/**
|
|
40
|
+
* Only count web clients whose verified actor principal matches this id.
|
|
41
|
+
* A client that connected without a principal (legacy or service token)
|
|
42
|
+
* never matches a supplied id.
|
|
43
|
+
*/
|
|
44
|
+
actorPrincipalId?: string;
|
|
45
|
+
/** Clock used for the staleness comparison. */
|
|
46
|
+
now?: Date;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether some web client has reported, recently enough to trust, that the
|
|
51
|
+
* given conversation is visible and focused. Stale, absent, hidden,
|
|
52
|
+
* unfocused, non-web, and error reads all answer `false`.
|
|
53
|
+
*
|
|
54
|
+
* Callers whose notification targets one recipient must pass that
|
|
55
|
+
* recipient's `actorPrincipalId`, or another user's focused tab suppresses
|
|
56
|
+
* the push. Omitting it treats any matching web client as focus, which only
|
|
57
|
+
* suits notifications with no single recipient.
|
|
58
|
+
*/
|
|
59
|
+
export function isWebConversationFocused(
|
|
60
|
+
conversationId: string,
|
|
61
|
+
options: WebPresenceOptions = {},
|
|
62
|
+
): boolean {
|
|
63
|
+
const { actorPrincipalId, now = new Date() } = options;
|
|
64
|
+
try {
|
|
65
|
+
return assistantEventHub.listClientsByInterface("web").some((client) => {
|
|
66
|
+
if (
|
|
67
|
+
actorPrincipalId !== undefined &&
|
|
68
|
+
client.actorPrincipalId !== actorPrincipalId
|
|
69
|
+
) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const report = client.webPresence;
|
|
73
|
+
if (!report) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const nowMs = now.getTime();
|
|
77
|
+
if (
|
|
78
|
+
nowMs - report.reportedAt.getTime() > WEB_PRESENCE_STALE_AFTER_MS ||
|
|
79
|
+
nowMs - client.lastActiveAt.getTime() > WEB_PRESENCE_STALE_AFTER_MS
|
|
80
|
+
) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return report.visible && report.focusedConversationId === conversationId;
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// Returning false sends the push, which is the safe direction.
|
|
87
|
+
log.warn({ err }, "web presence read failed; treating as unfocused");
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|