@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
|
@@ -3,7 +3,10 @@ import { join } from "node:path";
|
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
4
4
|
|
|
5
5
|
import type { RouteInvokeParams } from "../../../routes/route-host-protocol.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getWorkspacePluginsDir,
|
|
8
|
+
getWorkspaceRoutesDir,
|
|
9
|
+
} from "../../../util/platform.js";
|
|
7
10
|
|
|
8
11
|
// The dispatcher constructs the route host client inline and reads the enabled
|
|
9
12
|
// flag from config, so both are mocked here (there is no injection seam).
|
|
@@ -58,6 +61,16 @@ function writeHandler(name: string, content: string): void {
|
|
|
58
61
|
writeFileSync(join(getWorkspaceRoutesDir(), name), content);
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
function writePluginHandler(
|
|
65
|
+
pluginName: string,
|
|
66
|
+
name: string,
|
|
67
|
+
content: string,
|
|
68
|
+
): void {
|
|
69
|
+
const dir = join(getWorkspacePluginsDir(), pluginName, "routes");
|
|
70
|
+
mkdirSync(dir, { recursive: true });
|
|
71
|
+
writeFileSync(join(dir, name), content);
|
|
72
|
+
}
|
|
73
|
+
|
|
61
74
|
function jsonResponse(status: number, value: unknown): RouteInvokeResponse {
|
|
62
75
|
return {
|
|
63
76
|
status,
|
|
@@ -75,6 +88,7 @@ beforeEach(() => {
|
|
|
75
88
|
|
|
76
89
|
afterEach(() => {
|
|
77
90
|
rmSync(getWorkspaceRoutesDir(), { recursive: true, force: true });
|
|
91
|
+
rmSync(getWorkspacePluginsDir(), { recursive: true, force: true });
|
|
78
92
|
});
|
|
79
93
|
|
|
80
94
|
describe("UserRouteDispatcher — route host delegation", () => {
|
|
@@ -101,6 +115,43 @@ describe("UserRouteDispatcher — route host delegation", () => {
|
|
|
101
115
|
expect(invokeCalls[0].params.url).toContain("/v1/x/foo");
|
|
102
116
|
});
|
|
103
117
|
|
|
118
|
+
test("names the owning plugin so the host executes in its context", async () => {
|
|
119
|
+
// The host marks this plugin as in context for the handler, so
|
|
120
|
+
// plugin-scoped host APIs behave the same in the host and in-thread.
|
|
121
|
+
writePluginHandler(
|
|
122
|
+
"my-plugin",
|
|
123
|
+
"status.ts",
|
|
124
|
+
`export function GET() { return new Response(); }`,
|
|
125
|
+
);
|
|
126
|
+
hostEnabled = true;
|
|
127
|
+
const dispatcher = makeDispatcher();
|
|
128
|
+
|
|
129
|
+
await dispatcher.dispatch(
|
|
130
|
+
"plugins/my-plugin/status",
|
|
131
|
+
new Request("http://localhost/v1/x/plugins/my-plugin/status", {
|
|
132
|
+
method: "GET",
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
expect(invokeCalls[0].params.pluginName).toBe("my-plugin");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("leaves a workspace route unowned", async () => {
|
|
140
|
+
writeHandler(
|
|
141
|
+
"plain.ts",
|
|
142
|
+
`export function GET() { return new Response(); }`,
|
|
143
|
+
);
|
|
144
|
+
hostEnabled = true;
|
|
145
|
+
const dispatcher = makeDispatcher();
|
|
146
|
+
|
|
147
|
+
await dispatcher.dispatch(
|
|
148
|
+
"plain",
|
|
149
|
+
new Request("http://localhost/v1/x/plain", { method: "GET" }),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(invokeCalls[0].params.pluginName).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
104
155
|
test("runs in-band when disabled (host never called)", async () => {
|
|
105
156
|
writeHandler(
|
|
106
157
|
"bar.ts",
|
|
@@ -56,6 +56,15 @@ function writePluginHandler(
|
|
|
56
56
|
return fullPath;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Absolute specifier for the plugin execution context, so a handler written to
|
|
61
|
+
* a temp routes directory imports the same module instance this process holds.
|
|
62
|
+
*/
|
|
63
|
+
const PLUGIN_CONTEXT_MODULE = new URL(
|
|
64
|
+
"../../../plugins/plugin-execution-context.ts",
|
|
65
|
+
import.meta.url,
|
|
66
|
+
).href;
|
|
67
|
+
|
|
59
68
|
async function readErrorBody(
|
|
60
69
|
response: Response,
|
|
61
70
|
): Promise<{ error: { code: string; message: string } }> {
|
|
@@ -466,6 +475,71 @@ describe("plugin routes", () => {
|
|
|
466
475
|
expect(body.plugin).toBe(true);
|
|
467
476
|
});
|
|
468
477
|
|
|
478
|
+
test("executes the handler as the plugin that owns the namespace", async () => {
|
|
479
|
+
// Plugin-scoped host APIs (resolveCredential, indexDocument) read the
|
|
480
|
+
// execution context to decide what the caller may touch. A route handler
|
|
481
|
+
// that ran outside it would fall through to their unscoped branch and
|
|
482
|
+
// reach every plugin's credentials.
|
|
483
|
+
writePluginHandler(
|
|
484
|
+
"my-plugin",
|
|
485
|
+
"whoami.ts",
|
|
486
|
+
`import { getCurrentPluginName } from ${JSON.stringify(PLUGIN_CONTEXT_MODULE)};
|
|
487
|
+
export function GET(request) {
|
|
488
|
+
return Response.json({ plugin: getCurrentPluginName() ?? null });
|
|
489
|
+
}`,
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
const dispatcher = makeDispatcher();
|
|
493
|
+
const res = await dispatcher.dispatch(
|
|
494
|
+
"plugins/my-plugin/whoami",
|
|
495
|
+
makeRequest("GET"),
|
|
496
|
+
);
|
|
497
|
+
expect(res.status).toBe(200);
|
|
498
|
+
const body = await res.json();
|
|
499
|
+
expect(body.plugin).toBe("my-plugin");
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("evaluates the route module in the plugin's context too", async () => {
|
|
503
|
+
// A module can reach a scoped host API at import time, and the import
|
|
504
|
+
// happens once per mtime. A load left outside the context would take the
|
|
505
|
+
// unscoped branch and never be re-run scoped, so the context has to cover
|
|
506
|
+
// module evaluation and not just the handler call.
|
|
507
|
+
writePluginHandler(
|
|
508
|
+
"my-plugin",
|
|
509
|
+
"at-import.ts",
|
|
510
|
+
`import { getCurrentPluginName } from ${JSON.stringify(PLUGIN_CONTEXT_MODULE)};
|
|
511
|
+
const atImport = getCurrentPluginName() ?? null;
|
|
512
|
+
export function GET(request) {
|
|
513
|
+
return Response.json({ atImport });
|
|
514
|
+
}`,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const dispatcher = makeDispatcher();
|
|
518
|
+
const res = await dispatcher.dispatch(
|
|
519
|
+
"plugins/my-plugin/at-import",
|
|
520
|
+
makeRequest("GET"),
|
|
521
|
+
);
|
|
522
|
+
expect(res.status).toBe(200);
|
|
523
|
+
const body = await res.json();
|
|
524
|
+
expect(body.atImport).toBe("my-plugin");
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("leaves a workspace route outside any plugin context", async () => {
|
|
528
|
+
writeHandler(
|
|
529
|
+
"whoami.ts",
|
|
530
|
+
`import { getCurrentPluginName } from ${JSON.stringify(PLUGIN_CONTEXT_MODULE)};
|
|
531
|
+
export function GET(request) {
|
|
532
|
+
return Response.json({ plugin: getCurrentPluginName() ?? null });
|
|
533
|
+
}`,
|
|
534
|
+
);
|
|
535
|
+
|
|
536
|
+
const dispatcher = makeDispatcher();
|
|
537
|
+
const res = await dispatcher.dispatch("whoami", makeRequest("GET"));
|
|
538
|
+
expect(res.status).toBe(200);
|
|
539
|
+
const body = await res.json();
|
|
540
|
+
expect(body.plugin).toBeNull();
|
|
541
|
+
});
|
|
542
|
+
|
|
469
543
|
test("resolves nested paths and the index (namespace root)", async () => {
|
|
470
544
|
writePluginHandler(
|
|
471
545
|
"my-plugin",
|
|
@@ -583,10 +657,7 @@ describe("plugin routes", () => {
|
|
|
583
657
|
const plugin = "stale-helper";
|
|
584
658
|
const helperPath = join(getWorkspacePluginsDir(), plugin, "src", "http.ts");
|
|
585
659
|
mkdirSync(dirname(helperPath), { recursive: true });
|
|
586
|
-
writeFileSync(
|
|
587
|
-
helperPath,
|
|
588
|
-
`export function label() { return "v1"; }\n`,
|
|
589
|
-
);
|
|
660
|
+
writeFileSync(helperPath, `export function label() { return "v1"; }\n`);
|
|
590
661
|
writePluginHandler(
|
|
591
662
|
plugin,
|
|
592
663
|
"frame.ts",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
|
|
12
|
+
import { PendingToolQuestionSchema } from "../../api/responses/conversation-message.js";
|
|
12
13
|
import { findConversation } from "../../daemon/conversation-registry.js";
|
|
13
14
|
import type {
|
|
14
15
|
SecretDelivery,
|
|
@@ -210,9 +211,18 @@ function handleSecret({ body }: RouteHandlerArgs) {
|
|
|
210
211
|
* GET /v1/pending-interactions?conversationKey=...&conversationId=...
|
|
211
212
|
*
|
|
212
213
|
* Returns pending interactions. When conversationKey or conversationId is
|
|
213
|
-
* provided, returns the
|
|
214
|
-
* conversation. When neither is provided, returns all pending
|
|
215
|
-
* across all conversations (diagnostic mode).
|
|
214
|
+
* provided, returns the outstanding confirmation, secret, and question for
|
|
215
|
+
* that conversation. When neither is provided, returns all pending
|
|
216
|
+
* interactions across all conversations (diagnostic mode).
|
|
217
|
+
*
|
|
218
|
+
* This registry is the authority on whether a prompt is still awaiting the
|
|
219
|
+
* user, so the conversation-scoped shape is what clients project their prompt
|
|
220
|
+
* UI from: a key present with a `null` value states "nothing outstanding of
|
|
221
|
+
* this kind" as positively as an object states the opposite. Every
|
|
222
|
+
* conversation-scoped return path below therefore names all three keys, even
|
|
223
|
+
* the early return for an unresolvable conversation. A client that sees a key
|
|
224
|
+
* missing entirely is talking to an assistant that predates it, which is how
|
|
225
|
+
* `pendingQuestion` consumers detect support without a version check.
|
|
216
226
|
*/
|
|
217
227
|
function handleListPendingInteractions({ queryParams }: RouteHandlerArgs) {
|
|
218
228
|
const conversationKey = queryParams?.conversationKey;
|
|
@@ -241,7 +251,11 @@ function handleListPendingInteractions({ queryParams }: RouteHandlerArgs) {
|
|
|
241
251
|
}
|
|
242
252
|
|
|
243
253
|
if (!resolvedConversationId) {
|
|
244
|
-
return {
|
|
254
|
+
return {
|
|
255
|
+
pendingConfirmation: null,
|
|
256
|
+
pendingSecret: null,
|
|
257
|
+
pendingQuestion: null,
|
|
258
|
+
};
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
const interactions = pendingInteractions.getByConversation(
|
|
@@ -252,6 +266,13 @@ function handleListPendingInteractions({ queryParams }: RouteHandlerArgs) {
|
|
|
252
266
|
(i) => i.kind === "confirmation" || i.kind === "acp_confirmation",
|
|
253
267
|
);
|
|
254
268
|
const secret = interactions.find((i) => i.kind === "secret");
|
|
269
|
+
// Newest-first for questions: a new user message supersedes an open
|
|
270
|
+
// ask_question by steering to it, which settles the old prompt through its
|
|
271
|
+
// abort signal rather than synchronously, so both can sit in the registry
|
|
272
|
+
// for the width of that turn. The newer prompt is the one the user is being
|
|
273
|
+
// asked, so it is the one to report.
|
|
274
|
+
const questions = interactions.filter((i) => i.kind === "question");
|
|
275
|
+
const question = questions[questions.length - 1];
|
|
255
276
|
|
|
256
277
|
return {
|
|
257
278
|
pendingConfirmation: confirmation
|
|
@@ -288,6 +309,15 @@ function handleListPendingInteractions({ queryParams }: RouteHandlerArgs) {
|
|
|
288
309
|
allowOneTimeSend: secret.secretDetails?.allowOneTimeSend,
|
|
289
310
|
}
|
|
290
311
|
: null,
|
|
312
|
+
// Reported only with its `entries`, since the batch is what the card
|
|
313
|
+
// renders; a prompt that somehow carries none is not something a client
|
|
314
|
+
// could raise.
|
|
315
|
+
pendingQuestion: question?.questionDetails
|
|
316
|
+
? {
|
|
317
|
+
requestId: question.requestId,
|
|
318
|
+
entries: question.questionDetails.entries,
|
|
319
|
+
}
|
|
320
|
+
: null,
|
|
291
321
|
};
|
|
292
322
|
}
|
|
293
323
|
|
|
@@ -396,6 +426,9 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
396
426
|
.nullable()
|
|
397
427
|
.describe("Pending secret request or null")
|
|
398
428
|
.optional(),
|
|
429
|
+
pendingQuestion: PendingToolQuestionSchema.nullable()
|
|
430
|
+
.describe("Outstanding ask_question prompt or null")
|
|
431
|
+
.optional(),
|
|
399
432
|
interactions: z
|
|
400
433
|
.array(
|
|
401
434
|
z.object({
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* the conversational approval engine when an approvalConversationGenerator is
|
|
4
4
|
* available. Classifies natural language and responds conversationally.
|
|
5
5
|
*/
|
|
6
|
+
import { audienceForReader } from "../../../channels/message-audience.js";
|
|
6
7
|
import type { ChannelId } from "../../../channels/types.js";
|
|
7
8
|
import { getLogger } from "../../../util/logger.js";
|
|
8
9
|
import { runApprovalConversationTurn } from "../../approval-conversation-turn.js";
|
|
@@ -22,17 +23,6 @@ import { deliverStaleApprovalReply } from "../guardian-approval-reply-helpers.js
|
|
|
22
23
|
|
|
23
24
|
const log = getLogger("runtime-http");
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
-
* Resolve the Slack ephemeral user ID when the source channel is Slack.
|
|
27
|
-
* Returns `undefined` for non-Slack channels.
|
|
28
|
-
*/
|
|
29
|
-
function slackEphemeralUserId(
|
|
30
|
-
sourceChannel: ChannelId,
|
|
31
|
-
userId: string | undefined,
|
|
32
|
-
): string | undefined {
|
|
33
|
-
return sourceChannel === "slack" && userId ? userId : undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
26
|
export interface TextEngineDecisionParams {
|
|
37
27
|
conversationId: string;
|
|
38
28
|
conversationExternalId: string;
|
|
@@ -96,11 +86,11 @@ export async function handleGuardianTextEngineDecision(
|
|
|
96
86
|
text: engineResult.replyText,
|
|
97
87
|
assistantId,
|
|
98
88
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
89
|
+
keepPendingPayload.audience = audienceForReader(
|
|
90
|
+
sourceChannel,
|
|
91
|
+
conversationExternalId,
|
|
92
|
+
actorExternalId,
|
|
93
|
+
);
|
|
104
94
|
await deliverChannelReply(replyCallbackUrl, keepPendingPayload);
|
|
105
95
|
} catch (err) {
|
|
106
96
|
log.error(
|
|
@@ -131,11 +121,11 @@ export async function handleGuardianTextEngineDecision(
|
|
|
131
121
|
text: engineResult.replyText,
|
|
132
122
|
assistantId,
|
|
133
123
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
124
|
+
decisionPayload.audience = audienceForReader(
|
|
125
|
+
sourceChannel,
|
|
126
|
+
conversationExternalId,
|
|
127
|
+
actorExternalId,
|
|
128
|
+
);
|
|
139
129
|
await deliverChannelReply(replyCallbackUrl, decisionPayload);
|
|
140
130
|
} catch (err) {
|
|
141
131
|
log.error(
|
|
@@ -160,7 +150,11 @@ export async function handleGuardianTextEngineDecision(
|
|
|
160
150
|
logger: log,
|
|
161
151
|
errorLogMessage: "Failed to deliver stale approval notice",
|
|
162
152
|
errorLogContext: { conversationId },
|
|
163
|
-
|
|
153
|
+
audience: audienceForReader(
|
|
154
|
+
sourceChannel,
|
|
155
|
+
conversationExternalId,
|
|
156
|
+
actorExternalId,
|
|
157
|
+
),
|
|
164
158
|
});
|
|
165
159
|
|
|
166
160
|
return { handled: true, type: "stale_ignored" };
|
|
@@ -3,8 +3,8 @@ import type { AssistantEvent } from "../../api/index.js";
|
|
|
3
3
|
import {
|
|
4
4
|
addMessage,
|
|
5
5
|
recordConversationPersistedSeq,
|
|
6
|
-
SYSTEM_CARD_MESSAGE_KIND,
|
|
7
6
|
} from "../../persistence/conversation-crud.js";
|
|
7
|
+
import { SYSTEM_CARD_MESSAGE_KIND } from "../../persistence/conversation-types.js";
|
|
8
8
|
import type { Message } from "../../providers/types.js";
|
|
9
9
|
import { broadcastMessage } from "../assistant-event-hub.js";
|
|
10
10
|
import { getCurrentSeq } from "../assistant-stream-state.js";
|
|
@@ -40,33 +40,42 @@ export function emitCannedMessageComplete(
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* Persist a
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* merging never folds it into an adjacent assistant turn.
|
|
43
|
+
* Persist a system card: a daemon-authored notice that bypasses the agent
|
|
44
|
+
* loop. The row is stamped `messageKind: "system_card"` so transcripts render
|
|
45
|
+
* it as a standalone system notice instead of assistant-persona speech, and
|
|
46
|
+
* display merging never folds it into an adjacent assistant turn.
|
|
48
47
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* so the initiating client materializes it only by refetching, and origin
|
|
55
|
-
* self-echo suppression (meant for content the origin already rendered) would
|
|
56
|
-
* otherwise hide the card from the initiator until a reload. A delta is
|
|
57
|
-
* deliberately omitted; it would stream the card into the tail assistant
|
|
58
|
-
* bubble as if the persona were speaking.
|
|
48
|
+
* The card is persisted and announced to clients but not appended to the live
|
|
49
|
+
* conversation's working history, so a card written while a turn is in flight
|
|
50
|
+
* cannot leave a trailing assistant message for the next provider call to
|
|
51
|
+
* continue from. Callers whose card ends the turn use
|
|
52
|
+
* {@link persistCannedAssistantCard}, which also seats it in the live history.
|
|
59
53
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
54
|
+
* Every card advances the persisted-seq anchor (so a stale /messages reseed
|
|
55
|
+
* cannot erase it) and publishes the messages-changed sync invalidation that
|
|
56
|
+
* drives every client to refetch. That invalidation carries no origin-client
|
|
57
|
+
* id: the card body streams nowhere (no `assistant_text_delta`), so the
|
|
58
|
+
* initiating client materializes it only by refetching, and origin self-echo
|
|
59
|
+
* suppression (meant for content the origin already rendered) would otherwise
|
|
60
|
+
* hide the card from the initiator until a reload. A delta is deliberately
|
|
61
|
+
* omitted; it would stream the card into the tail assistant bubble as if the
|
|
62
|
+
* persona were speaking.
|
|
63
|
+
*
|
|
64
|
+
* `endsTurn` decides whether the card also emits `message_complete`. Clients
|
|
65
|
+
* treat that event as terminal (it clears the processing state and closes the
|
|
66
|
+
* turn), so only a card that *is* the reply carries it. A card posted while a
|
|
67
|
+
* turn is still running (`endsTurn: false`) rides the sync invalidation alone,
|
|
68
|
+
* leaving the in-flight turn's streaming state intact.
|
|
69
|
+
*
|
|
70
|
+
* Returns the persisted card's id and the in-memory message it persisted.
|
|
62
71
|
*/
|
|
63
|
-
export async function
|
|
64
|
-
conversation: { getMessages(): Message[] };
|
|
72
|
+
export async function persistSystemCard(opts: {
|
|
65
73
|
conversationId: string;
|
|
66
74
|
text: string;
|
|
67
75
|
metadata: Record<string, unknown>;
|
|
68
|
-
|
|
69
|
-
|
|
76
|
+
endsTurn: boolean;
|
|
77
|
+
}): Promise<{ id: string; message: Message }> {
|
|
78
|
+
const { conversationId, text, metadata, endsTurn } = opts;
|
|
70
79
|
const assistantMsg = createAssistantMessage(text);
|
|
71
80
|
const persistedAssistant = await addMessage(
|
|
72
81
|
conversationId,
|
|
@@ -74,13 +83,43 @@ export async function persistCannedAssistantCard(opts: {
|
|
|
74
83
|
JSON.stringify(assistantMsg.content),
|
|
75
84
|
{ metadata: { ...metadata, messageKind: SYSTEM_CARD_MESSAGE_KIND } },
|
|
76
85
|
);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
if (endsTurn) {
|
|
87
|
+
emitCannedMessageComplete(
|
|
88
|
+
broadcastMessage,
|
|
89
|
+
conversationId,
|
|
90
|
+
persistedAssistant.id,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
83
93
|
recordConversationPersistedSeq(conversationId, getCurrentSeq());
|
|
84
94
|
publishConversationMessagesChanged(conversationId);
|
|
85
|
-
return persistedAssistant.id;
|
|
95
|
+
return { id: persistedAssistant.id, message: assistantMsg };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Persist a canned assistant "card" (a pre-composed reply that bypasses the
|
|
100
|
+
* agent loop: the /compact, /clean, and summarize-up-to result cards) and
|
|
101
|
+
* seat it in the live conversation's history so the turn that follows sees it.
|
|
102
|
+
* The row is stamped `messageKind: "system_card"` so transcripts render it as a
|
|
103
|
+
* standalone system notice instead of assistant-persona speech, and display
|
|
104
|
+
* merging never folds it into an adjacent assistant turn. Delivery is
|
|
105
|
+
* {@link persistSystemCard}'s.
|
|
106
|
+
*
|
|
107
|
+
* Returns the persisted card's message id so callers can link related
|
|
108
|
+
* records to it (e.g. the compaction `llm_request_logs` row).
|
|
109
|
+
*/
|
|
110
|
+
export async function persistCannedAssistantCard(opts: {
|
|
111
|
+
conversation: { getMessages(): Message[] };
|
|
112
|
+
conversationId: string;
|
|
113
|
+
text: string;
|
|
114
|
+
metadata: Record<string, unknown>;
|
|
115
|
+
}): Promise<string> {
|
|
116
|
+
const { conversation, conversationId, text, metadata } = opts;
|
|
117
|
+
const card = await persistSystemCard({
|
|
118
|
+
conversationId,
|
|
119
|
+
text,
|
|
120
|
+
metadata,
|
|
121
|
+
endsTurn: true,
|
|
122
|
+
});
|
|
123
|
+
conversation.getMessages().push(card.message);
|
|
124
|
+
return card.id;
|
|
86
125
|
}
|
|
@@ -37,6 +37,23 @@ const PresenceBodySchema = z.object({
|
|
|
37
37
|
.describe("Reported desktop presence state."),
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Body of `POST /v1/clients/web-presence`, declared as the route's
|
|
42
|
+
* `requestBody` and parsed by the handler, so the OpenAPI contract and the
|
|
43
|
+
* runtime check are one schema rather than two hand-kept copies.
|
|
44
|
+
*/
|
|
45
|
+
const WebPresenceBodySchema = z.object({
|
|
46
|
+
visible: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.describe("Whether the reporting tab is currently visible."),
|
|
49
|
+
focusedConversationId: z
|
|
50
|
+
.string()
|
|
51
|
+
.nullable()
|
|
52
|
+
.describe(
|
|
53
|
+
"The conversation currently focused (chat composer on screen) in the tab, or null.",
|
|
54
|
+
),
|
|
55
|
+
});
|
|
56
|
+
|
|
40
57
|
export const ROUTES: RouteDefinition[] = [
|
|
41
58
|
{
|
|
42
59
|
operationId: "list_clients",
|
|
@@ -228,4 +245,100 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
228
245
|
return { recorded: true };
|
|
229
246
|
},
|
|
230
247
|
},
|
|
248
|
+
{
|
|
249
|
+
operationId: "report_web_presence",
|
|
250
|
+
endpoint: "clients/web-presence",
|
|
251
|
+
method: "POST",
|
|
252
|
+
policy: {
|
|
253
|
+
requiredScopes: ["settings.write"],
|
|
254
|
+
allowedPrincipalTypes: ACTOR_PRINCIPALS,
|
|
255
|
+
},
|
|
256
|
+
summary: "Report web tab visibility and focused conversation",
|
|
257
|
+
description:
|
|
258
|
+
"Record the visibility and focused conversation reported by the web client identified by the X-Vellum-Client-Id header.",
|
|
259
|
+
tags: ["clients"],
|
|
260
|
+
requestBody: WebPresenceBodySchema,
|
|
261
|
+
responseBody: z.object({
|
|
262
|
+
recorded: z
|
|
263
|
+
.boolean()
|
|
264
|
+
.describe("Whether a connected client matched the reporting clientId."),
|
|
265
|
+
}),
|
|
266
|
+
handler: ({ body, headers }) => {
|
|
267
|
+
const clientId = headers?.["x-vellum-client-id"]?.trim();
|
|
268
|
+
if (!clientId) {
|
|
269
|
+
throw new BadRequestError(
|
|
270
|
+
"x-vellum-client-id header is required to report presence.",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const { visible, focusedConversationId } = parseBody(
|
|
274
|
+
WebPresenceBodySchema,
|
|
275
|
+
body,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// Resolving the client first separates "nobody is listening" from
|
|
279
|
+
// "somebody else owns this", so only the second one is worth a warn.
|
|
280
|
+
const client = assistantEventHub.getClientById(clientId);
|
|
281
|
+
if (!client) {
|
|
282
|
+
// Debug, not warn: a report racing an SSE reconnect matches nothing,
|
|
283
|
+
// and a reconnect storm would otherwise emit a warn per client for a
|
|
284
|
+
// benign race. Not a 404 either, for the same reason.
|
|
285
|
+
log.debug(
|
|
286
|
+
{ op: "report_web_presence" },
|
|
287
|
+
"Web presence report matched no connected client",
|
|
288
|
+
);
|
|
289
|
+
return { recorded: false };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Only the actor that opened the target client's SSE stream may report
|
|
293
|
+
// its presence, so a caller who learns another user's clientId cannot
|
|
294
|
+
// spoof that tab. Clients with no stored `actorPrincipalId` (legacy SSE
|
|
295
|
+
// subscribers, service-gateway tokens) never match: fail-closed, the
|
|
296
|
+
// same posture as `report_client_presence`. Dev-bypass mode
|
|
297
|
+
// (DISABLE_HTTP_AUTH=true) skips the check for platform-managed
|
|
298
|
+
// deployments where the platform handles auth.
|
|
299
|
+
if (!isHttpAuthDisabled()) {
|
|
300
|
+
const callerPrincipalId = headers?.["x-vellum-actor-principal-id"];
|
|
301
|
+
const ownerPrincipalId = client.actorPrincipalId;
|
|
302
|
+
if (
|
|
303
|
+
ownerPrincipalId === undefined ||
|
|
304
|
+
ownerPrincipalId !== callerPrincipalId
|
|
305
|
+
) {
|
|
306
|
+
// Warn, not debug: the client is connected, so a mismatch means the
|
|
307
|
+
// gateway stopped forwarding the actor header, a scope profile
|
|
308
|
+
// shifted, or a caller is probing someone else's client. Without
|
|
309
|
+
// this line presence gating dies silently and every suppressed
|
|
310
|
+
// push quietly resumes. Client and principal ids stay out of the
|
|
311
|
+
// entry so the log cannot be used to enumerate them.
|
|
312
|
+
log.warn(
|
|
313
|
+
{
|
|
314
|
+
op: "report_web_presence",
|
|
315
|
+
hasStoredOwner: ownerPrincipalId !== undefined,
|
|
316
|
+
hasCallerPrincipal: callerPrincipalId !== undefined,
|
|
317
|
+
targetInterfaceId: client.interfaceId,
|
|
318
|
+
},
|
|
319
|
+
"Rejecting web presence report from a caller that does not own the client",
|
|
320
|
+
);
|
|
321
|
+
// Answering like "no match" keeps the reply from probing client ids.
|
|
322
|
+
return { recorded: false };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// The lookup ran in this same synchronous handler, so the write lands on
|
|
327
|
+
// the entry it resolved.
|
|
328
|
+
assistantEventHub.setClientWebPresence(clientId, {
|
|
329
|
+
visible,
|
|
330
|
+
focusedConversationId,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
log.debug(
|
|
334
|
+
{
|
|
335
|
+
op: "report_web_presence",
|
|
336
|
+
visible,
|
|
337
|
+
interfaceId: client.interfaceId,
|
|
338
|
+
},
|
|
339
|
+
"Recorded web presence",
|
|
340
|
+
);
|
|
341
|
+
return { recorded: true };
|
|
342
|
+
},
|
|
343
|
+
},
|
|
231
344
|
];
|
|
@@ -269,12 +269,18 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
|
|
|
269
269
|
? true
|
|
270
270
|
: undefined;
|
|
271
271
|
|
|
272
|
+
const foregroundOnly =
|
|
273
|
+
parseEnumQueryParam(queryParams, "foregroundOnly", ["true"]) === "true"
|
|
274
|
+
? true
|
|
275
|
+
: undefined;
|
|
276
|
+
|
|
272
277
|
const filter: ConversationListFilter = {
|
|
273
278
|
conversationType,
|
|
274
279
|
archiveStatus,
|
|
275
280
|
originChannel,
|
|
276
281
|
groupId,
|
|
277
282
|
needsAttention,
|
|
283
|
+
foregroundOnly,
|
|
278
284
|
};
|
|
279
285
|
let rows = listConversations({ ...filter, limit, offset });
|
|
280
286
|
const totalCount = countConversations(filter);
|
|
@@ -289,19 +295,20 @@ function handleListConversations({ queryParams = {} }: RouteHandlerArgs) {
|
|
|
289
295
|
// Skipped for group-scoped queries for the same reason: a caller asking
|
|
290
296
|
// for one group gets that group, and a client that fetches the Pinned
|
|
291
297
|
// section via `groupId=system:pinned` has no use for rows appended to
|
|
292
|
-
// some other group's page. Skipped for attention-scoped
|
|
293
|
-
// caller asked for
|
|
294
|
-
//
|
|
295
|
-
// computed from the filtered count. This is the
|
|
296
|
-
// clients that still read Pinned out of the
|
|
297
|
-
// away once every section fetches its own group.
|
|
298
|
+
// some other group's page. Skipped for attention-scoped and
|
|
299
|
+
// foreground-only queries too: the caller asked for a subset, and a
|
|
300
|
+
// pinned row outside it appended to the page would sit there while
|
|
301
|
+
// `hasMore` is computed from the filtered count. This is the
|
|
302
|
+
// compatibility shim for clients that still read Pinned out of the
|
|
303
|
+
// unfiltered list; it goes away once every section fetches its own group.
|
|
298
304
|
if (
|
|
299
305
|
offset === 0 &&
|
|
300
306
|
conversationType === "standard" &&
|
|
301
307
|
archiveStatus === "active" &&
|
|
302
308
|
originChannel === undefined &&
|
|
303
309
|
groupId === undefined &&
|
|
304
|
-
needsAttention === undefined
|
|
310
|
+
needsAttention === undefined &&
|
|
311
|
+
foregroundOnly === undefined
|
|
305
312
|
) {
|
|
306
313
|
const pinned = listPinnedConversations(archiveStatus);
|
|
307
314
|
const seen = new Set(rows.map((c) => c.id));
|
|
@@ -611,6 +618,14 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
611
618
|
'Pass "true" to return only conversations whose latest assistant message the user has not seen: the same predicate behind the unread count and the section index, so a client that keeps no complete conversation list can ask for exactly the rows its attention surfaces need. Composes with every other filter. Any value other than "true" is rejected. Omit to span every conversation.',
|
|
612
619
|
schema: { type: "string", enum: ["true"] },
|
|
613
620
|
},
|
|
621
|
+
{
|
|
622
|
+
name: "foregroundOnly",
|
|
623
|
+
type: "string",
|
|
624
|
+
required: false,
|
|
625
|
+
description:
|
|
626
|
+
'Pass "true" to return only user-facing foreground conversations, dropping the automated background and scheduled runs the standard listing admits when they are filed in a custom group (a surfaced run stays). This is the same predicate the unread count and the section index apply, so a client can ask for the newest conversation a user can open in one row instead of paging past runs it would skip. Composes with every other filter; a foreground-only first page never has pinned rows appended to it. Any value other than "true" is rejected. Omit to keep every visible row.',
|
|
627
|
+
schema: { type: "string", enum: ["true"] },
|
|
628
|
+
},
|
|
614
629
|
],
|
|
615
630
|
responseBody: listConversationsResponseSchema,
|
|
616
631
|
handler: handleListConversations,
|