@vellumai/assistant 0.11.4 → 0.11.5-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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__/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__/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/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/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/feature-flag-registry.json +35 -19
- 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
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "../persistence/conversation-types.js";
|
|
30
30
|
import { stringifyMessageContent } from "../persistence/message-content.js";
|
|
31
31
|
import { isDesktopAttended } from "../runtime/desktop-presence.js";
|
|
32
|
+
import { isWebConversationFocused } from "../runtime/web-presence.js";
|
|
32
33
|
import { safeParseRecord } from "../util/json.js";
|
|
33
34
|
import { emitNotificationSignal } from "./emit-signal.js";
|
|
34
35
|
import {
|
|
@@ -45,6 +46,9 @@ const ASSISTANT_REPLY_PUSH_FLAG = "assistant-reply-push" as const;
|
|
|
45
46
|
/** Gates the desktop-attended suppression below, on by default. */
|
|
46
47
|
const DESKTOP_PRESENCE_FLAG = "desktop-presence-suppression" as const;
|
|
47
48
|
|
|
49
|
+
/** Gates the web-focused suppression below, on by default. */
|
|
50
|
+
const WEB_PRESENCE_FLAG = "web-presence-suppression" as const;
|
|
51
|
+
|
|
48
52
|
/**
|
|
49
53
|
* Collapse whitespace runs ahead of the sanitizers' truncation: blank lines and
|
|
50
54
|
* list indentation would otherwise eat into the copy's length budget.
|
|
@@ -129,6 +133,27 @@ function readDesktopAttended(rlog: pino.Logger): boolean {
|
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Web presence, kept fail-open here for the same reason as
|
|
138
|
+
* {@link readDesktopAttended}: a presence read that fails has to send the
|
|
139
|
+
* push, not reach the producer's catch and silence it.
|
|
140
|
+
*
|
|
141
|
+
* No `actorPrincipalId`: the platform delivers this push to the assistant
|
|
142
|
+
* owner's device tokens, and a pod has exactly one owner, so any focused web
|
|
143
|
+
* tab is that owner's.
|
|
144
|
+
*/
|
|
145
|
+
function readWebConversationFocused(
|
|
146
|
+
conversationId: string,
|
|
147
|
+
rlog: pino.Logger,
|
|
148
|
+
): boolean {
|
|
149
|
+
try {
|
|
150
|
+
return isWebConversationFocused(conversationId);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
rlog.warn({ err }, "Web presence read failed; treating as unfocused");
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
132
157
|
export async function emitAssistantReplyNotification(params: {
|
|
133
158
|
conversationId: string;
|
|
134
159
|
assistantMessageId: string;
|
|
@@ -229,6 +254,14 @@ export async function emitAssistantReplyNotification(params: {
|
|
|
229
254
|
isMacOriginatedUserMessage(initiatingMetadata) &&
|
|
230
255
|
readDesktopAttended(rlog);
|
|
231
256
|
|
|
257
|
+
// Conversation-scoped web presence applies regardless of which device
|
|
258
|
+
// initiated the turn: a visible matching tab proves where this reply is
|
|
259
|
+
// currently being displayed, while the conversation id prevents an
|
|
260
|
+
// unrelated tab from suppressing the push.
|
|
261
|
+
const webFocused =
|
|
262
|
+
isAssistantFeatureFlagEnabled(WEB_PRESENCE_FLAG) &&
|
|
263
|
+
readWebConversationFocused(conversationId, rlog);
|
|
264
|
+
|
|
232
265
|
await emitNotificationSignal({
|
|
233
266
|
sourceEventName: "chat.assistant_reply",
|
|
234
267
|
sourceChannel: "vellum",
|
|
@@ -243,10 +276,10 @@ export async function emitAssistantReplyNotification(params: {
|
|
|
243
276
|
// opting into v2.
|
|
244
277
|
urgency: "medium",
|
|
245
278
|
isAsyncBackground: false,
|
|
246
|
-
// Read weakly, as "at the
|
|
247
|
-
// that opened the turn renders the reply in-app,
|
|
248
|
-
//
|
|
249
|
-
visibleInSourceNow: desktopAttended,
|
|
279
|
+
// Read weakly, as "at the surface this landed on": the attended Mac
|
|
280
|
+
// or focused web tab that opened the turn renders the reply in-app,
|
|
281
|
+
// so a redundant push would only duplicate what's already on screen.
|
|
282
|
+
visibleInSourceNow: desktopAttended || webFocused,
|
|
250
283
|
},
|
|
251
284
|
contextPayload: {
|
|
252
285
|
...(requestedTitle ? { requestedTitle } : {}),
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
getBindingByChannelChat,
|
|
31
31
|
upsertOutboundBinding,
|
|
32
32
|
} from "../persistence/external-conversation-store.js";
|
|
33
|
+
import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
|
|
33
34
|
import { getLogger } from "../util/logger.js";
|
|
34
35
|
import { withSqliteRetry } from "../util/sqlite-retry.js";
|
|
35
36
|
import {
|
|
@@ -92,6 +93,13 @@ export interface PairingOptions {
|
|
|
92
93
|
*
|
|
93
94
|
* Invalid/stale targets at any level fall through to the next.
|
|
94
95
|
*
|
|
96
|
+
* Passive vellum notifications (those without `requiresConversation`) never
|
|
97
|
+
* reach that order. They create nothing, and instead append their body to the
|
|
98
|
+
* producing conversation named by `sourceContextId` when it resolves, since
|
|
99
|
+
* that is the row the home feed's "Go to Conversation" button already targets.
|
|
100
|
+
* The returned `conversationId` is that producing conversation, with
|
|
101
|
+
* `createdNewConversation` false.
|
|
102
|
+
*
|
|
95
103
|
* Errors are caught and logged — this function never throws so the
|
|
96
104
|
* notification pipeline is not disrupted by pairing failures.
|
|
97
105
|
*/
|
|
@@ -117,20 +125,47 @@ export async function pairDeliveryWithConversation(
|
|
|
117
125
|
const conversationAction = options?.conversationAction;
|
|
118
126
|
const bindingContext = options?.bindingContext;
|
|
119
127
|
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
//
|
|
128
|
+
// Structured content blocks take precedence. They enable Surface-based
|
|
129
|
+
// rendering in the web/macOS/iOS apps (e.g. a card widget instead of
|
|
130
|
+
// plain text). Falls back to model-provided seed or runtime composer.
|
|
131
|
+
const messageContent = copy.seedContentBlocks
|
|
132
|
+
? JSON.stringify(copy.seedContentBlocks)
|
|
133
|
+
: isConversationSeedSane(copy.conversationSeedMessage)
|
|
134
|
+
? copy.conversationSeedMessage
|
|
135
|
+
: composeConversationSeed(signal, channel, copy);
|
|
136
|
+
|
|
137
|
+
// Passive vellum notifications link back to the originating conversation
|
|
138
|
+
// via `signal.sourceContextId` rather than materializing one of their own.
|
|
139
|
+
// A fresh per-notification conversation just to host the seed message
|
|
140
|
+
// leaves a graveyard entry in the sidebar, so nothing is created here when
|
|
141
|
+
// the producer did not opt in via `requiresConversation`. The decision
|
|
142
|
+
// engine's `reuse_existing` hint is ignored for the same reason: a failed
|
|
143
|
+
// reuse (stale target / source mismatch) falls through to
|
|
144
|
+
// `createConversation`, producing exactly the graveyard entry we want to
|
|
145
|
+
// avoid.
|
|
146
|
+
//
|
|
147
|
+
// The body is still appended to the producing conversation when
|
|
148
|
+
// `sourceContextId` resolves, because every route the user has into this
|
|
149
|
+
// notification ends at that conversation. Tapping the banner deep-links
|
|
150
|
+
// there, and resolves there with or without this append, since the
|
|
151
|
+
// broadcaster falls back to `sourceContextId` for the vellum deep link.
|
|
152
|
+
// The client suppresses the banner outright when that conversation is
|
|
153
|
+
// already on screen, leaving the transcript as the only place the
|
|
154
|
+
// notification can appear. The home feed aims its "Go to Conversation"
|
|
155
|
+
// button at the same row whenever it mirrors the signal.
|
|
156
|
+
//
|
|
157
|
+
// So this is deliberately not gated on home-feed eligibility: a signal the
|
|
158
|
+
// feed declines to mirror still reaches the user through the banner, and
|
|
159
|
+
// this row is what makes that landing honest.
|
|
130
160
|
if (strategy === "start_new_conversation" && !signal.requiresConversation) {
|
|
161
|
+
const appended = await appendBodyToSourceConversation(
|
|
162
|
+
signal,
|
|
163
|
+
channel,
|
|
164
|
+
messageContent,
|
|
165
|
+
);
|
|
131
166
|
return {
|
|
132
|
-
conversationId: null,
|
|
133
|
-
messageId: null,
|
|
167
|
+
conversationId: appended?.conversationId ?? null,
|
|
168
|
+
messageId: appended?.messageId ?? null,
|
|
134
169
|
strategy,
|
|
135
170
|
createdNewConversation: false,
|
|
136
171
|
conversationFallbackUsed: false,
|
|
@@ -147,15 +182,6 @@ export async function pairDeliveryWithConversation(
|
|
|
147
182
|
signal.conversationMetadata?.conversationType ??
|
|
148
183
|
(strategy === "start_new_conversation" ? "standard" : "background");
|
|
149
184
|
|
|
150
|
-
// Structured content blocks take precedence — they enable Surface-based
|
|
151
|
-
// rendering in the web/macOS/iOS apps (e.g. a card widget instead of
|
|
152
|
-
// plain text). Falls back to model-provided seed or runtime composer.
|
|
153
|
-
const messageContent = copy.seedContentBlocks
|
|
154
|
-
? JSON.stringify(copy.seedContentBlocks)
|
|
155
|
-
: isConversationSeedSane(copy.conversationSeedMessage)
|
|
156
|
-
? copy.conversationSeedMessage
|
|
157
|
-
: composeConversationSeed(signal, channel, copy);
|
|
158
|
-
|
|
159
185
|
// Attempt to reuse an existing conversation when the model requests it
|
|
160
186
|
if (conversationAction?.action === "reuse_existing") {
|
|
161
187
|
const targetId = conversationAction.conversationId;
|
|
@@ -456,3 +482,67 @@ export async function pairDeliveryWithConversation(
|
|
|
456
482
|
};
|
|
457
483
|
}
|
|
458
484
|
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Append a delivered notification body to the conversation that produced it.
|
|
488
|
+
*
|
|
489
|
+
* Passive notifications never materialize a conversation of their own, so the
|
|
490
|
+
* home feed points "Go to Conversation" at the producing conversation
|
|
491
|
+
* (`resolveHomeFeedMirror` prefers `sourceContextId`). Writing the body there
|
|
492
|
+
* makes that button truthful: whenever it renders, the conversation it opens
|
|
493
|
+
* contains the notification the user tapped.
|
|
494
|
+
*
|
|
495
|
+
* Reuse only. Producers may pass sentinels (job ids, call session ids,
|
|
496
|
+
* `access-req-*` strings) as `sourceContextId`; those resolve to nothing and
|
|
497
|
+
* append nothing, which matches the button staying hidden for them.
|
|
498
|
+
*
|
|
499
|
+
* This covers vellum deliveries only, since no other channel takes the
|
|
500
|
+
* passive branch. A signal routed away from vellum still gets a card, so
|
|
501
|
+
* `writeHomeFeedItemForSignal` writes the body itself when no vellum
|
|
502
|
+
* conversation was paired. Between them the button holds whatever the
|
|
503
|
+
* routing was, and only one of the two ever writes.
|
|
504
|
+
*
|
|
505
|
+
* Indexing is skipped for parity with the other notification write paths:
|
|
506
|
+
* notification copy is delivery audit, not conversational memory.
|
|
507
|
+
*/
|
|
508
|
+
async function appendBodyToSourceConversation(
|
|
509
|
+
signal: NotificationSignal,
|
|
510
|
+
channel: NotificationChannel,
|
|
511
|
+
messageContent: string,
|
|
512
|
+
): Promise<{ conversationId: string; messageId: string } | null> {
|
|
513
|
+
const sourceContextId = signal.sourceContextId;
|
|
514
|
+
if (!sourceContextId) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
let existing: ReturnType<typeof getConversation>;
|
|
519
|
+
try {
|
|
520
|
+
existing = getConversation(sourceContextId);
|
|
521
|
+
} catch {
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
if (!existing) {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const message = await addMessage(existing.id, "assistant", messageContent, {
|
|
529
|
+
skipIndexing: true,
|
|
530
|
+
});
|
|
531
|
+
// `addMessage` projects attention metadata alone, so a client with this
|
|
532
|
+
// conversation open needs the messages tag to refetch the transcript. A
|
|
533
|
+
// notification the user taps through to has every chance of landing on an
|
|
534
|
+
// already-open conversation.
|
|
535
|
+
publishConversationMessagesChanged(existing.id);
|
|
536
|
+
|
|
537
|
+
log.info(
|
|
538
|
+
{
|
|
539
|
+
signalId: signal.signalId,
|
|
540
|
+
channel,
|
|
541
|
+
conversationId: existing.id,
|
|
542
|
+
messageId: message.id,
|
|
543
|
+
},
|
|
544
|
+
"Appended notification body to producing conversation",
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
return { conversationId: existing.id, messageId: message.id };
|
|
548
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
updateDeliveryRenderedCopy,
|
|
23
23
|
} from "./deliveries-store.js";
|
|
24
24
|
import { getBroadcaster } from "./emit-signal.js";
|
|
25
|
+
import { updateFeedItemConversationMessage } from "./home-feed-side-effect.js";
|
|
25
26
|
import { nonEmpty } from "./notification-utils.js";
|
|
26
27
|
import type { NotificationChannel } from "./types.js";
|
|
27
28
|
|
|
@@ -119,24 +120,49 @@ export async function editNotification(
|
|
|
119
120
|
{ feedItemId, signalId },
|
|
120
121
|
"Feed item has no persisted decision — skipping channel updates",
|
|
121
122
|
);
|
|
122
|
-
return { feedItem, channels: [] };
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
const deliveries = findDeliveriesByDecisionId(decision.id);
|
|
126
|
-
const channels = await updateChannelDeliveries(
|
|
127
|
-
|
|
128
|
-
body: params.body,
|
|
129
|
-
|
|
125
|
+
const deliveries = decision ? findDeliveriesByDecisionId(decision.id) : [];
|
|
126
|
+
const { channels, rewrittenMessageIds } = await updateChannelDeliveries(
|
|
127
|
+
deliveries,
|
|
128
|
+
{ title, body: params.body },
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// The delivery walk covers a conversation row only while its delivery reads
|
|
132
|
+
// sent, so the card carries the id of the row behind it and this closes
|
|
133
|
+
// whatever the walk missed. Runs after it, and skips a row the walk just
|
|
134
|
+
// rewrote, so the two never both write. Body edits only: a title-only patch
|
|
135
|
+
// leaves the feed summary alone, and the row holds the body.
|
|
136
|
+
if (params.body !== undefined) {
|
|
137
|
+
updateFeedItemConversationMessage(
|
|
138
|
+
feedItem,
|
|
139
|
+
params.body,
|
|
140
|
+
rewrittenMessageIds,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
130
143
|
|
|
131
144
|
return { feedItem, channels };
|
|
132
145
|
}
|
|
133
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Walk the recorded deliveries and let each channel adapter apply the patch.
|
|
149
|
+
*
|
|
150
|
+
* Alongside the per-channel outcomes this reports the message ids the
|
|
151
|
+
* adapters rewrote, which is what tells the caller whether the row behind the
|
|
152
|
+
* card still needs writing. Only body patches contribute: an adapter reports
|
|
153
|
+
* its message id on a title-only patch too, without having touched the
|
|
154
|
+
* conversation row.
|
|
155
|
+
*/
|
|
134
156
|
async function updateChannelDeliveries(
|
|
135
157
|
deliveries: NotificationDeliveryRow[],
|
|
136
158
|
patch: { title?: string; body?: string },
|
|
137
|
-
): Promise<
|
|
159
|
+
): Promise<{
|
|
160
|
+
channels: ChannelEditResult[];
|
|
161
|
+
rewrittenMessageIds: ReadonlySet<string>;
|
|
162
|
+
}> {
|
|
138
163
|
const broadcaster = getBroadcaster();
|
|
139
164
|
const results: ChannelEditResult[] = [];
|
|
165
|
+
const rewrittenMessageIds = new Set<string>();
|
|
140
166
|
|
|
141
167
|
for (const delivery of deliveries) {
|
|
142
168
|
const channel = delivery.channel as NotificationChannel;
|
|
@@ -167,6 +193,7 @@ async function updateChannelDeliveries(
|
|
|
167
193
|
deliveryId: delivery.id,
|
|
168
194
|
destination: delivery.destination,
|
|
169
195
|
messageId: delivery.messageId,
|
|
196
|
+
conversationId: delivery.conversationId,
|
|
170
197
|
},
|
|
171
198
|
patch,
|
|
172
199
|
);
|
|
@@ -183,6 +210,9 @@ async function updateChannelDeliveries(
|
|
|
183
210
|
renderedTitle: patch.title,
|
|
184
211
|
renderedBody: patch.body,
|
|
185
212
|
});
|
|
213
|
+
if (patch.body !== undefined && result.messageId) {
|
|
214
|
+
rewrittenMessageIds.add(result.messageId);
|
|
215
|
+
}
|
|
186
216
|
results.push({
|
|
187
217
|
channel,
|
|
188
218
|
deliveryId: delivery.id,
|
|
@@ -203,5 +233,5 @@ async function updateChannelDeliveries(
|
|
|
203
233
|
}
|
|
204
234
|
}
|
|
205
235
|
|
|
206
|
-
return results;
|
|
236
|
+
return { channels: results, rewrittenMessageIds };
|
|
207
237
|
}
|
|
@@ -525,21 +525,19 @@ export async function emitNotificationSignal<TEventName extends string>(
|
|
|
525
525
|
// Step 5: Mirror background-origin signals into the home activity feed.
|
|
526
526
|
// The helper itself decides whether to write (background filter); we
|
|
527
527
|
// catch and log so a feed-write failure cannot poison the dispatch result.
|
|
528
|
-
// Pass the
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
const
|
|
528
|
+
// Pass the vellum delivery outcome so producers whose `sourceContextId` is
|
|
529
|
+
// a sentinel string (e.g. heartbeat startup, credential health, watcher
|
|
530
|
+
// emits, scheduler retries-exhausted) still get a "Go to Convo" button
|
|
531
|
+
// pointing at the conversation the broadcaster paired, and so the feed can
|
|
532
|
+
// tell whether that delivery left a conversation row an edit can reach.
|
|
533
|
+
const vellumDelivery = dispatchResult.deliveryResults.find(
|
|
534
534
|
(r) => r.channel === "vellum",
|
|
535
|
-
)
|
|
536
|
-
await writeHomeFeedItemForSignal(
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
)
|
|
541
|
-
log.warn({ err, signalId }, "writeHomeFeedItemForSignal threw");
|
|
542
|
-
});
|
|
535
|
+
);
|
|
536
|
+
await writeHomeFeedItemForSignal(signal, decision, vellumDelivery).catch(
|
|
537
|
+
(err) => {
|
|
538
|
+
log.warn({ err, signalId }, "writeHomeFeedItemForSignal threw");
|
|
539
|
+
},
|
|
540
|
+
);
|
|
543
541
|
|
|
544
542
|
log.info(
|
|
545
543
|
{
|
|
@@ -17,31 +17,59 @@ import {
|
|
|
17
17
|
feedItemSchema,
|
|
18
18
|
} from "../home/feed-types.js";
|
|
19
19
|
import { appendFeedItem } from "../home/feed-writer.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
addMessage,
|
|
22
|
+
getConversation,
|
|
23
|
+
getMessageById,
|
|
24
|
+
updateMessageContent,
|
|
25
|
+
} from "../persistence/conversation-crud.js";
|
|
21
26
|
import { isBackgroundConversationType } from "../persistence/conversation-types.js";
|
|
27
|
+
import { publishConversationMessagesChanged } from "../runtime/sync/resource-sync-events.js";
|
|
22
28
|
import { getLogger } from "../util/logger.js";
|
|
23
29
|
import { normalizeTitle, stripMarkdown } from "../util/short-title.js";
|
|
24
30
|
import { isConversationSeedSane } from "./conversation-seed-composer.js";
|
|
25
31
|
import { deriveTitle } from "./copy-composer.js";
|
|
26
32
|
import { readPayloadString } from "./notification-utils.js";
|
|
27
33
|
import type { NotificationSignal } from "./signal.js";
|
|
28
|
-
import type {
|
|
34
|
+
import type {
|
|
35
|
+
NotificationDecision,
|
|
36
|
+
NotificationDeliveryResult,
|
|
37
|
+
RenderedChannelCopy,
|
|
38
|
+
} from "./types.js";
|
|
29
39
|
|
|
30
40
|
const log = getLogger("home-feed-side-effect");
|
|
31
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Metadata key holding the id of the conversation message this module wrote
|
|
44
|
+
* for a card.
|
|
45
|
+
*
|
|
46
|
+
* Only cards holding a row no delivery adapter will rewrite carry it, and it
|
|
47
|
+
* is the handle `updateFeedItemConversationMessage` rewrites on an edit.
|
|
48
|
+
* `metadata` is a free-form record on the wire, so this stays a daemon-side
|
|
49
|
+
* convention and needs no schema field, the same way `scheduleId` rides along
|
|
50
|
+
* below.
|
|
51
|
+
*
|
|
52
|
+
* Reserved: producer `contextPayload` also lands in `metadata`, so the key is
|
|
53
|
+
* stripped from it before this module writes its own. A producer able to set
|
|
54
|
+
* it would otherwise hand an edit an arbitrary message id to overwrite.
|
|
55
|
+
*/
|
|
56
|
+
const CONVERSATION_MESSAGE_ID_KEY = "notificationConversationMessageId";
|
|
57
|
+
|
|
32
58
|
/**
|
|
33
59
|
* Append a `FeedItem` for the given notification signal when the
|
|
34
60
|
* filter criteria pass.
|
|
35
61
|
*
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* producers
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
62
|
+
* `vellumDelivery` is this signal's vellum delivery outcome, and it settles
|
|
63
|
+
* two questions here.
|
|
64
|
+
*
|
|
65
|
+
* Its conversation is a navigation target standing in for a
|
|
66
|
+
* `signal.sourceContextId` that doesn't resolve to a real conversation row,
|
|
67
|
+
* so producers passing a sentinel (heartbeat startup, credential health,
|
|
68
|
+
* watcher emits, scheduler retries-exhausted) still render a "Go to Convo"
|
|
69
|
+
* button.
|
|
70
|
+
*
|
|
71
|
+
* It also settles which row backs the card and who rewrites it on an edit,
|
|
72
|
+
* which `resolveOwnedConversationMessageId` works through below.
|
|
45
73
|
*
|
|
46
74
|
* Returns the persisted `FeedItem`, or `null` if the signal does not
|
|
47
75
|
* qualify for home-feed mirroring (non-background origin AND no
|
|
@@ -50,10 +78,10 @@ const log = getLogger("home-feed-side-effect");
|
|
|
50
78
|
export async function writeHomeFeedItemForSignal(
|
|
51
79
|
signal: NotificationSignal,
|
|
52
80
|
decision: NotificationDecision,
|
|
53
|
-
|
|
81
|
+
vellumDelivery?: NotificationDeliveryResult,
|
|
54
82
|
): Promise<FeedItem | null> {
|
|
55
83
|
const { mirror, sourceConversationId, sourceScheduleJobId } =
|
|
56
|
-
resolveHomeFeedMirror(signal,
|
|
84
|
+
resolveHomeFeedMirror(signal, vellumDelivery?.conversationId);
|
|
57
85
|
if (!mirror) {
|
|
58
86
|
return null;
|
|
59
87
|
}
|
|
@@ -109,6 +137,9 @@ export async function writeHomeFeedItemForSignal(
|
|
|
109
137
|
!Array.isArray(signal.contextPayload)
|
|
110
138
|
? { ...signal.contextPayload }
|
|
111
139
|
: undefined;
|
|
140
|
+
// Producer payloads reach the card verbatim, and this key addresses a
|
|
141
|
+
// message row for rewriting, so only this module may set it.
|
|
142
|
+
delete baseMetadata?.[CONVERSATION_MESSAGE_ID_KEY];
|
|
112
143
|
|
|
113
144
|
// Link scheduled-run notifications back to their schedule. `notify`-mode
|
|
114
145
|
// jobs put `scheduleId` directly in the context payload; `execute`-mode (and
|
|
@@ -152,8 +183,180 @@ export async function writeHomeFeedItemForSignal(
|
|
|
152
183
|
return null;
|
|
153
184
|
}
|
|
154
185
|
|
|
155
|
-
|
|
156
|
-
|
|
186
|
+
// Resolved once the card is known to be writable, since the only reason to
|
|
187
|
+
// put a notification body in a conversation is that a card sends the user
|
|
188
|
+
// there. Validating first keeps the one rejection this function controls
|
|
189
|
+
// from leaving a message behind with no card to explain it. The id joins
|
|
190
|
+
// free-form metadata, which no schema rule can turn away.
|
|
191
|
+
const conversationMessageId = await resolveOwnedConversationMessageId(
|
|
192
|
+
signal,
|
|
193
|
+
vellumDelivery,
|
|
194
|
+
sourceConversationId,
|
|
195
|
+
resolvedSummary,
|
|
196
|
+
);
|
|
197
|
+
const card: FeedItem = conversationMessageId
|
|
198
|
+
? {
|
|
199
|
+
...item,
|
|
200
|
+
metadata: {
|
|
201
|
+
...(item.metadata ?? {}),
|
|
202
|
+
[CONVERSATION_MESSAGE_ID_KEY]: conversationMessageId,
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
: item;
|
|
206
|
+
|
|
207
|
+
await appendFeedItem(card);
|
|
208
|
+
return card;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Resolve the conversation row backing this card, writing one if the card
|
|
213
|
+
* would otherwise have no body in the conversation it opens.
|
|
214
|
+
*
|
|
215
|
+
* The card records the row whoever wrote it, rather than only the rows it
|
|
216
|
+
* expects an edit to miss. Whether a delivery adapter will actually rewrite a
|
|
217
|
+
* row is a property of the durable delivery state at edit time, which a
|
|
218
|
+
* dispatch result cannot predict: a send can succeed and lose its status
|
|
219
|
+
* write, leaving a row that reads pending. `editNotification` settles that
|
|
220
|
+
* question by rewriting through the card only when the delivery walk did not.
|
|
221
|
+
*
|
|
222
|
+
* A vellum delivery that paired a conversation already wrote the body there,
|
|
223
|
+
* so its row is the one to record. Guardian producers pair a fresh
|
|
224
|
+
* conversation instead of the one the card opens, and their row is left
|
|
225
|
+
* unrecorded: a handle has to address the conversation behind the button, and
|
|
226
|
+
* `updateFeedItemConversationMessage` refuses anything else.
|
|
227
|
+
*/
|
|
228
|
+
async function resolveOwnedConversationMessageId(
|
|
229
|
+
signal: NotificationSignal,
|
|
230
|
+
vellumDelivery: NotificationDeliveryResult | undefined,
|
|
231
|
+
sourceConversationId: string | undefined,
|
|
232
|
+
summary: string,
|
|
233
|
+
): Promise<string | undefined> {
|
|
234
|
+
if (vellumDelivery?.conversationId) {
|
|
235
|
+
return vellumDelivery.conversationId === sourceConversationId
|
|
236
|
+
? vellumDelivery.messageId
|
|
237
|
+
: undefined;
|
|
238
|
+
}
|
|
239
|
+
if (!sourceConversationId) {
|
|
240
|
+
return undefined;
|
|
241
|
+
}
|
|
242
|
+
return appendSummaryToFeedTarget(signal, sourceConversationId, summary);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Write the card's own summary into the conversation its "Go to Convo"
|
|
247
|
+
* button opens, returning the id of the row written.
|
|
248
|
+
*
|
|
249
|
+
* That button targets the producing conversation whatever the routing, but
|
|
250
|
+
* only the vellum delivery writes a notification body into a conversation
|
|
251
|
+
* (`pairDeliveryWithConversation` appends there for passive signals). A
|
|
252
|
+
* signal the decision engine routes to Slack or Telegram alone pairs no
|
|
253
|
+
* vellum conversation, so without this the button opens a conversation that
|
|
254
|
+
* never received what the card shows. A paired vellum id means that write
|
|
255
|
+
* already happened, and this is skipped.
|
|
256
|
+
*
|
|
257
|
+
* The caller has already read `sourceConversationId` back from the store to
|
|
258
|
+
* gate the mirror, so the row is known to exist and needs no second lookup:
|
|
259
|
+
* with no paired id to fall back on, a resolved target can only be the
|
|
260
|
+
* producing conversation itself.
|
|
261
|
+
*
|
|
262
|
+
* Indexing is skipped for parity with the other notification write paths:
|
|
263
|
+
* notification copy is delivery audit, not conversational memory. Failures
|
|
264
|
+
* are logged rather than thrown, so a conversation write cannot cost the
|
|
265
|
+
* user a card; the card just goes out without a rewritable row.
|
|
266
|
+
*/
|
|
267
|
+
async function appendSummaryToFeedTarget(
|
|
268
|
+
signal: NotificationSignal,
|
|
269
|
+
conversationId: string,
|
|
270
|
+
summary: string,
|
|
271
|
+
): Promise<string | undefined> {
|
|
272
|
+
try {
|
|
273
|
+
const message = await addMessage(conversationId, "assistant", summary, {
|
|
274
|
+
skipIndexing: true,
|
|
275
|
+
});
|
|
276
|
+
publishConversationMessagesChanged(conversationId);
|
|
277
|
+
log.info(
|
|
278
|
+
{
|
|
279
|
+
signalId: signal.signalId,
|
|
280
|
+
conversationId,
|
|
281
|
+
messageId: message.id,
|
|
282
|
+
},
|
|
283
|
+
"Appended notification body to the feed item's conversation",
|
|
284
|
+
);
|
|
285
|
+
return message.id;
|
|
286
|
+
} catch (err) {
|
|
287
|
+
log.warn(
|
|
288
|
+
{ err, signalId: signal.signalId, conversationId },
|
|
289
|
+
"Failed to append notification body to the feed item's conversation",
|
|
290
|
+
);
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Rewrite the conversation message behind a card, keeping an edited card and
|
|
297
|
+
* the conversation it opens in step.
|
|
298
|
+
*
|
|
299
|
+
* Notification edits reach conversation content through the delivery rows the
|
|
300
|
+
* broadcaster recorded, which is how the vellum adapter rewrites the row its
|
|
301
|
+
* pairing wrote. That walk covers a row only while its delivery reads sent, so
|
|
302
|
+
* this is the backstop for every way it can miss one: a delivery that failed,
|
|
303
|
+
* a row that was never recorded, a signal with no deliveries at all, and a
|
|
304
|
+
* send whose status write was lost after the message went out.
|
|
305
|
+
*
|
|
306
|
+
* `alreadyRewritten` carries the message ids the delivery walk reported
|
|
307
|
+
* rewriting, so the common case where the adapter got there first costs
|
|
308
|
+
* nothing and no row is written twice.
|
|
309
|
+
*
|
|
310
|
+
* The row holds the body, and the feed rewrites its summary only when the
|
|
311
|
+
* patch carries one, so the caller applies this for body edits alone.
|
|
312
|
+
*
|
|
313
|
+
* Never throws. It runs last, after the feed patch and the channel updates
|
|
314
|
+
* have landed, so anything it cannot do is reported as "no rewrite" rather
|
|
315
|
+
* than failing an edit that has already partly applied. Returns whether a row
|
|
316
|
+
* was rewritten.
|
|
317
|
+
*/
|
|
318
|
+
export function updateFeedItemConversationMessage(
|
|
319
|
+
item: FeedItem,
|
|
320
|
+
body: string,
|
|
321
|
+
alreadyRewritten?: ReadonlySet<string>,
|
|
322
|
+
): boolean {
|
|
323
|
+
const messageId = item.metadata?.[CONVERSATION_MESSAGE_ID_KEY];
|
|
324
|
+
if (typeof messageId !== "string" || messageId.length === 0) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
if (alreadyRewritten?.has(messageId)) {
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
// Scoped to the card's own conversation so the handle can only ever
|
|
332
|
+
// address a row inside what the card opens, whatever put it in the
|
|
333
|
+
// metadata. Guarded with the write: a store that cannot answer the
|
|
334
|
+
// question is not grounds for failing an edit already applied to the feed
|
|
335
|
+
// and the channels.
|
|
336
|
+
if (
|
|
337
|
+
!item.conversationId ||
|
|
338
|
+
!getMessageById(messageId, item.conversationId)
|
|
339
|
+
) {
|
|
340
|
+
log.warn(
|
|
341
|
+
{ feedItemId: item.id, messageId },
|
|
342
|
+
"Feed item message handle does not address a row in its conversation, leaving it alone",
|
|
343
|
+
);
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
updateMessageContent(messageId, body);
|
|
347
|
+
publishConversationMessagesChanged(item.conversationId);
|
|
348
|
+
log.info(
|
|
349
|
+
{ feedItemId: item.id, messageId },
|
|
350
|
+
"Rewrote the conversation message behind an edited feed item",
|
|
351
|
+
);
|
|
352
|
+
return true;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
log.error(
|
|
355
|
+
{ err, feedItemId: item.id, messageId },
|
|
356
|
+
"Failed to rewrite the conversation message behind an edited feed item",
|
|
357
|
+
);
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
157
360
|
}
|
|
158
361
|
|
|
159
362
|
/**
|
|
@@ -150,6 +150,11 @@ export interface ChannelUpdateContext {
|
|
|
150
150
|
deliveryId: string;
|
|
151
151
|
destination: string;
|
|
152
152
|
messageId: string | null;
|
|
153
|
+
/**
|
|
154
|
+
* Conversation the delivery paired, for adapters whose message lives in one.
|
|
155
|
+
* Channel-native adapters patch a channel message and leave this unread.
|
|
156
|
+
*/
|
|
157
|
+
conversationId?: string | null;
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
// -- Conversation action types ------------------------------------------------
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Permissions: Agent Instructions
|
|
2
|
+
|
|
3
|
+
This directory is the assistant's **client** of gateway-owned risk classification and trust rules, plus the assistant-owned decision that follows. Keep the split:
|
|
4
|
+
|
|
5
|
+
- **Gateway owns:** risk classification (`gateway/src/risk/*`, entered through the `classify_risk` IPC method), the trust rules that lower or raise a classified risk (applied inside the gateway classifiers and surfaced only as the classified level plus `matchType: "user_rule"`), the auto-approve thresholds and channel-permission cells, and the per-actor `TrustClass` verdict.
|
|
6
|
+
- **Assistant owns:** the turn's actor and its capabilities (`runtime/capabilities.ts`), the sensitive-tool capability floor (`tools/tool-approval-handler.ts`), the allow / prompt / deny decision over risk × threshold × capabilities (`check()` here, `DefaultApprovalPolicy`), the prompt UX (`prompter.ts`), execution, and the `tool_invocations` audit row.
|
|
7
|
+
|
|
8
|
+
Rules that follow from the split:
|
|
9
|
+
|
|
10
|
+
- **Do not add a risk classifier, risk table, or trust-rule matcher under `assistant/src`.** If a tool's risk is wrong or a rule cannot cover it, the fix is in `gateway/src/risk/` (or the tool's `defaultRiskLevel`, which the gateway uses for tools without a dedicated classifier).
|
|
11
|
+
- **Classify once per tool invocation.** `classifyRisk()` is the only call into the gateway; the executor takes it before the pre-execution gates and hands the result to `checkPermission` and `check()`. Do not re-classify downstream, and do not memoise the result in the daemon: the inputs (rules, config, skill content) change out from under it, and the gateway already memoises the expensive part.
|
|
12
|
+
- **Read the classification, do not re-derive it.** The allowlist ladder is `classification.allowlistOptions`; there are no per-tool option builders here. Elevations, `matchType` checks, and option ladders come from the `RiskClassificationWithMeta` the gateway returned. A daemon-side "defense in depth" copy of a gateway rule is a second source of truth, not a defense.
|
|
13
|
+
- **Types cross the boundary from the wire contract.** The `classify_risk` request and response are `ClassifyRiskIpcParamsSchema` / `ClassifyRiskIpcResponseSchema` in `@vellumai/gateway-client`; fields the daemon carries from the response are typed off `ClassifyRiskIpcResponse`, never re-declared. `src/__tests__/risk-classification-boundary-guard.test.ts` pins the single call site and the absence of local classifiers.
|
|
14
|
+
- The one daemon-side adjustment to a classification is the bash sandbox symlink-escape re-check (`applyBashSymlinkEscapeCheck`), because it needs the assistant's filesystem; it only ever revokes auto-approve.
|
|
15
|
+
|
|
16
|
+
The architecture doc for this area is [`docs/architecture/security.md`](../../docs/architecture/security.md).
|