@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
|
@@ -73,7 +73,7 @@ type ProfileImpls = Record<DefaultProfileKey, DefaultProfileTemplate>;
|
|
|
73
73
|
*/
|
|
74
74
|
const VELLUM_PROFILE_IMPLS: ProfileImpls = {
|
|
75
75
|
balanced: {
|
|
76
|
-
model: "
|
|
76
|
+
model: "accounts/fireworks/models/glm-5p2",
|
|
77
77
|
provider: "vellum",
|
|
78
78
|
source: "managed",
|
|
79
79
|
label: "Balanced",
|
|
@@ -130,8 +130,7 @@ const VELLUM_PROFILE_IMPLS: ProfileImpls = {
|
|
|
130
130
|
// provisioned in every environment, and it alone selects the upstream:
|
|
131
131
|
// `provider` below is the provider-agnostic managed sentinel, so
|
|
132
132
|
// `getManagedUpstream` resolves the real upstream from the model's catalog
|
|
133
|
-
// owner.
|
|
134
|
-
// and thinking, and this model is the one live-voice TTFT drives validated.
|
|
133
|
+
// owner. This model is the one live-voice TTFT drives validated.
|
|
135
134
|
model: "gpt-5.6-luna",
|
|
136
135
|
provider: "vellum",
|
|
137
136
|
source: "managed",
|
|
@@ -165,8 +164,8 @@ const VELLUM_PROFILE_IMPLS: ProfileImpls = {
|
|
|
165
164
|
* arm is remote input, and an object lookup would resolve `constructor` or
|
|
166
165
|
* `toString` to an inherited `Object.prototype` member instead of missing.
|
|
167
166
|
*
|
|
168
|
-
* `glm-5p2`
|
|
169
|
-
* arm
|
|
167
|
+
* `glm-5p2` names the same model as the shipped pin and stays in the table so
|
|
168
|
+
* the arm keeps its meaning if the shipped pin moves again.
|
|
170
169
|
*/
|
|
171
170
|
const BALANCED_EXPERIMENT_MODELS = new Map<string, string>([
|
|
172
171
|
["terra", "gpt-5.6-terra"],
|
|
@@ -206,7 +205,7 @@ const CHATGPT_PROFILE_IMPLS: ProfileImpls = {
|
|
|
206
205
|
source: "managed",
|
|
207
206
|
label: "Balanced",
|
|
208
207
|
description: "Good balance of quality, cost, and speed",
|
|
209
|
-
// Matches the vellum column's Balanced
|
|
208
|
+
// Matches the vellum column's Balanced token budget: the Codex path
|
|
210
209
|
// sends no max_output_tokens, so this only sizes internal budgeting.
|
|
211
210
|
maxTokens: 32000,
|
|
212
211
|
effort: "high",
|
|
@@ -92,14 +92,6 @@
|
|
|
92
92
|
"description": "Enable the Quick Input popover on right-click of the menu bar icon",
|
|
93
93
|
"defaultEnabled": false
|
|
94
94
|
},
|
|
95
|
-
{
|
|
96
|
-
"id": "companion-surface",
|
|
97
|
-
"scope": "client",
|
|
98
|
-
"key": "companion-surface",
|
|
99
|
-
"label": "Companion Surface",
|
|
100
|
-
"description": "Show the macOS companion surface: the floating avatar that carries Talk, Type and the running call, and its tray item. Read in the Electron main process, which decides whether the window opens at all",
|
|
101
|
-
"defaultEnabled": false
|
|
102
|
-
},
|
|
103
95
|
{
|
|
104
96
|
"id": "expand-completed-steps",
|
|
105
97
|
"scope": "client",
|
|
@@ -108,14 +100,6 @@
|
|
|
108
100
|
"description": "Auto-expand completed tool call step groups instead of showing them collapsed",
|
|
109
101
|
"defaultEnabled": false
|
|
110
102
|
},
|
|
111
|
-
{
|
|
112
|
-
"id": "collapse-assistant-intermediates",
|
|
113
|
-
"scope": "client",
|
|
114
|
-
"key": "collapse-assistant-intermediates",
|
|
115
|
-
"label": "Collapse Assistant Intermediates",
|
|
116
|
-
"description": "Collapse completed intermediary assistant activity behind Earlier activity while keeping the final response visible",
|
|
117
|
-
"defaultEnabled": false
|
|
118
|
-
},
|
|
119
103
|
{
|
|
120
104
|
"id": "backward-releases",
|
|
121
105
|
"scope": "assistant",
|
|
@@ -152,8 +136,16 @@
|
|
|
152
136
|
"id": "fork-from-message",
|
|
153
137
|
"scope": "client",
|
|
154
138
|
"key": "fork-from-message",
|
|
155
|
-
"label": "
|
|
156
|
-
"description": "
|
|
139
|
+
"label": "Internal Thread Actions (legacy key)",
|
|
140
|
+
"description": "Legacy key for `internal-thread-actions`. The client grants the internal thread affordances when either key is enabled, so the platform may serve the gate under either name. Declared here so the flag mapper keeps the value instead of dropping it as undeclared.",
|
|
141
|
+
"defaultEnabled": false
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"id": "internal-thread-actions",
|
|
145
|
+
"scope": "client",
|
|
146
|
+
"key": "internal-thread-actions",
|
|
147
|
+
"label": "Internal Thread Actions",
|
|
148
|
+
"description": "Show the internal thread affordances: 'Fork from here' on messages, 'Fork Conversation', 'Analyze Conversation', and 'Copy conversation ID' in the thread menu, plus the LLM inspector surfaces. The flag is the sole gate: any session that enables it gets the affordances, including local sessions with no platform identity.",
|
|
157
149
|
"defaultEnabled": false
|
|
158
150
|
},
|
|
159
151
|
{
|
|
@@ -237,11 +229,11 @@
|
|
|
237
229
|
"defaultEnabled": false
|
|
238
230
|
},
|
|
239
231
|
{
|
|
240
|
-
"id": "
|
|
232
|
+
"id": "paired-devices-ui",
|
|
241
233
|
"scope": "client",
|
|
242
|
-
"key": "
|
|
243
|
-
"label": "
|
|
244
|
-
"description": "Show the pair-a-device card
|
|
234
|
+
"key": "paired-devices-ui",
|
|
235
|
+
"label": "Paired Devices UI",
|
|
236
|
+
"description": "Show the paired-devices list and revoke controls inside the pair-a-device settings card. Visibility only; `vellum devices` on the host stays available.",
|
|
245
237
|
"defaultEnabled": false
|
|
246
238
|
},
|
|
247
239
|
{
|
|
@@ -324,14 +316,6 @@
|
|
|
324
316
|
"description": "Persist chat secret redactions as sentinel markers instead of the legacy HTML marker, enriched with the vault identity when the value byte-matches a credential revealed this turn. Also gates the `credentials reveal --for-chat` channel and the live-stream swap that replaces an echoed reveal plaintext with its sentinel before emission. Redaction itself is NOT gated: a route-proven reveal plaintext stays out of persisted rows in both modes. Client chip rendering is gated on daemon version, not on this flag.",
|
|
325
317
|
"defaultEnabled": false
|
|
326
318
|
},
|
|
327
|
-
{
|
|
328
|
-
"id": "assistant-switcher",
|
|
329
|
-
"scope": "client",
|
|
330
|
-
"key": "assistant-switcher",
|
|
331
|
-
"label": "Assistant Switcher",
|
|
332
|
-
"description": "Show the Switch Assistant section in General settings, linking to the assistant chooser. Covers local clients, signed-in cloud-hub users, remote-gateway origins, and native mobile shells; remote-gateway desktop browsers hand off to the hub chooser with a self-registration param. Also makes the chooser's selection authoritative for the app's active assistant on every surface including the platform hub, a job that previously belonged to multi-platform-assistant alone.",
|
|
333
|
-
"defaultEnabled": false
|
|
334
|
-
},
|
|
335
319
|
{
|
|
336
320
|
"id": "marketing-pricing-takeover",
|
|
337
321
|
"scope": "client",
|
|
@@ -356,22 +340,38 @@
|
|
|
356
340
|
"description": "Gates whether a `chat.assistant_reply` push is suppressed while a Mac desktop client reports itself attended. On by default; turn it off to restore unconditional reply pushes.",
|
|
357
341
|
"defaultEnabled": true
|
|
358
342
|
},
|
|
343
|
+
{
|
|
344
|
+
"id": "web-presence-suppression",
|
|
345
|
+
"scope": "assistant",
|
|
346
|
+
"key": "web-presence-suppression",
|
|
347
|
+
"label": "Web Presence Suppression",
|
|
348
|
+
"description": "Gates whether a `chat.assistant_reply` push is suppressed while a browser tab reports itself visible on that conversation. On by default; turn it off to restore unconditional reply pushes for web-visible conversations.",
|
|
349
|
+
"defaultEnabled": true
|
|
350
|
+
},
|
|
359
351
|
{
|
|
360
352
|
"id": "plugin-schedules",
|
|
361
353
|
"scope": "assistant",
|
|
362
354
|
"key": "plugin-schedules",
|
|
363
355
|
"label": "Plugin Schedules",
|
|
364
356
|
"description": "Plugins declare recurring schedules as files that register and run automatically",
|
|
365
|
-
"defaultEnabled":
|
|
357
|
+
"defaultEnabled": true
|
|
366
358
|
},
|
|
367
359
|
{
|
|
368
360
|
"id": "experiment-balanced-model-2026-08-06",
|
|
369
361
|
"scope": "assistant",
|
|
370
362
|
"key": "experiment-balanced-model-2026-08-06",
|
|
371
363
|
"label": "Experiment: Balanced Profile Model",
|
|
372
|
-
"description": "Multivariate experiment on the model the managed Balanced inference profile resolves to. control =
|
|
364
|
+
"description": "Multivariate experiment on the model the managed Balanced inference profile resolves to. control = the shipped pin (accounts/fireworks/models/glm-5p2); terra = gpt-5.6-terra; glm-5p2 = accounts/fireworks/models/glm-5p2. Only the managed (vellum) implementation moves: ChatGPT-subscription and BYOK installs run the provider the user chose and are outside the experiment. Rollout and targeting are managed in the LaunchDarkly dashboard; any value that is not an arm name resolves to control.",
|
|
373
365
|
"defaultEnabled": "control",
|
|
374
366
|
"values": ["control", "terra", "glm-5p2"]
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"id": "obscure-credits",
|
|
370
|
+
"scope": "client",
|
|
371
|
+
"key": "obscure-credits",
|
|
372
|
+
"label": "Obscure Credits",
|
|
373
|
+
"description": "Replaces the dollar amounts on the billing Plan section with a usage-relative view: the current-plan tile trades its price footer for a Usage Balance bar (managed spend this billing cycle against the package's included credits) and the credits spec chip reads \"<Package> usage, reset monthly\" instead of naming a dollar bundle. Spec chips wrap horizontally to fit the shorter labels. Off leaves the Plan section exactly as it ships today.",
|
|
374
|
+
"defaultEnabled": false
|
|
375
375
|
}
|
|
376
376
|
]
|
|
377
377
|
}
|
|
@@ -83,6 +83,14 @@ function isIngressExplicitlyDisabled(): boolean {
|
|
|
83
83
|
*
|
|
84
84
|
* `allowManagedCallbacks` gates both platform tiers: channels that can only be
|
|
85
85
|
* served by a self-hosted ingress pass `false` and never see them.
|
|
86
|
+
*
|
|
87
|
+
* The gateway's Telegram webhook reconciler implements this same resolution
|
|
88
|
+
* order when deciding what URL to hand Telegram's setWebhook
|
|
89
|
+
* (`resolveExpectedTelegramWebhookUrl` in
|
|
90
|
+
* `gateway/src/telegram/webhook-manager.ts`). The two must stay in agreement:
|
|
91
|
+
* a tier this derivation reports as configured but the reconciler declines
|
|
92
|
+
* leaves Telegram setup reporting success while no webhook is ever
|
|
93
|
+
* registered. Change the tiers in both places or not at all.
|
|
86
94
|
*/
|
|
87
95
|
export async function hasWebhookRoutingConfigured(
|
|
88
96
|
allowManagedCallbacks = false,
|
package/src/context/compactor.ts
CHANGED
|
@@ -39,6 +39,10 @@ import type {
|
|
|
39
39
|
} from "../providers/types.js";
|
|
40
40
|
import { type TrustClass } from "../runtime/actor-trust-resolver.js";
|
|
41
41
|
import { resolveCapabilities } from "../runtime/capabilities.js";
|
|
42
|
+
import {
|
|
43
|
+
hasValidJpegStructure,
|
|
44
|
+
sniffImageMimeType,
|
|
45
|
+
} from "../util/image-conversion.js";
|
|
42
46
|
import { getLogger } from "../util/logger.js";
|
|
43
47
|
import { preModelCallSanitize } from "./outbound-sanitize.js";
|
|
44
48
|
import { stripInjectionsForCompaction } from "./strip-injections.js";
|
|
@@ -865,7 +869,8 @@ function resolveTailFloorIndex(messages: Message[], tailIndex: number): number {
|
|
|
865
869
|
// Retained-image hydration
|
|
866
870
|
// ---------------------------------------------------------------------------
|
|
867
871
|
|
|
868
|
-
|
|
872
|
+
/** Exported for unit testing (the corrupt-bytes drop gate). */
|
|
873
|
+
export async function buildRetainedImageBlocks(
|
|
869
874
|
filenames: string[],
|
|
870
875
|
manifest: ManifestEntry[],
|
|
871
876
|
): Promise<{ blocks: ImageContent[]; resolved: string[]; missing: string[] }> {
|
|
@@ -883,7 +888,11 @@ async function buildRetainedImageBlocks(
|
|
|
883
888
|
missing.push(name);
|
|
884
889
|
continue;
|
|
885
890
|
}
|
|
886
|
-
|
|
891
|
+
// Sniff the declared MIME from the stored bytes rather than the filename:
|
|
892
|
+
// clients derive extensions from user input, and providers reject an image
|
|
893
|
+
// whose bytes disagree with the declared media type.
|
|
894
|
+
const sourceMime =
|
|
895
|
+
sniffImageMimeType(content) ?? guessMimeFromFilename(name);
|
|
887
896
|
// Run the same downscale pass the agent uses when first sending an
|
|
888
897
|
// image. Without this, attachments that exceed the provider's per-image
|
|
889
898
|
// byte limit (Anthropic: 5 MB) crash the next turn after compaction.
|
|
@@ -891,6 +900,26 @@ async function buildRetainedImageBlocks(
|
|
|
891
900
|
content.toString("base64"),
|
|
892
901
|
sourceMime,
|
|
893
902
|
);
|
|
903
|
+
// Last-line gate before the bytes are baked into the rebuilt history: a
|
|
904
|
+
// corrupt payload would be rejected by the provider with a 400 on EVERY
|
|
905
|
+
// subsequent turn, wedging the conversation. Dropping the image loses a
|
|
906
|
+
// retained picture; shipping it loses the conversation. Format sniffing
|
|
907
|
+
// alone is not enough for JPEG: a truncated JPEG (e.g. persisted from a
|
|
908
|
+
// torn conversion-cache read) keeps its SOI header, so JPEG payloads must
|
|
909
|
+
// also walk to a terminal EOI marker.
|
|
910
|
+
const optimizedBytes = Buffer.from(optimized.data, "base64");
|
|
911
|
+
const optimizedFormat = sniffImageMimeType(optimizedBytes);
|
|
912
|
+
const bytesAreValidImage =
|
|
913
|
+
optimizedFormat != null &&
|
|
914
|
+
(optimizedFormat !== "image/jpeg" ||
|
|
915
|
+
hasValidJpegStructure(optimizedBytes));
|
|
916
|
+
if (!bytesAreValidImage) {
|
|
917
|
+
log.warn(
|
|
918
|
+
{ filename: name, attachmentId: entry.attachmentId },
|
|
919
|
+
"Retained image bytes are not a valid image after transport optimization; dropping",
|
|
920
|
+
);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
894
923
|
blocks.push({
|
|
895
924
|
type: "image",
|
|
896
925
|
source: {
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests that an unclassified 4xx provider rejection logs the upstream
|
|
3
|
+
* response body (bounded) plus the offending request content-part coordinates,
|
|
4
|
+
* so the failing message can be identified from the daemon log alone.
|
|
5
|
+
*/
|
|
6
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
7
|
+
|
|
8
|
+
import type { AssistantEvent } from "../../api/index.js";
|
|
9
|
+
import { ProviderError } from "../../util/errors.js";
|
|
10
|
+
import * as loggerModule from "../../util/logger.js";
|
|
11
|
+
import type {
|
|
12
|
+
EventHandlerDeps,
|
|
13
|
+
EventHandlerState,
|
|
14
|
+
} from "../conversation-agent-loop-handlers.js";
|
|
15
|
+
import {
|
|
16
|
+
buildProviderRejectionLogFields,
|
|
17
|
+
MAX_LOGGED_UPSTREAM_BODY_CHARS,
|
|
18
|
+
} from "../provider-rejection-log-fields.js";
|
|
19
|
+
|
|
20
|
+
const logRecords: Record<string, unknown>[] = [];
|
|
21
|
+
|
|
22
|
+
mock.module("../../util/logger.js", () => ({
|
|
23
|
+
...loggerModule,
|
|
24
|
+
getLogger: () => ({
|
|
25
|
+
error: (record: Record<string, unknown>) => {
|
|
26
|
+
logRecords.push(record);
|
|
27
|
+
},
|
|
28
|
+
warn: () => {},
|
|
29
|
+
info: () => {},
|
|
30
|
+
debug: () => {},
|
|
31
|
+
trace: () => {},
|
|
32
|
+
}),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
// The handlers module binds its logger at import time, so it is loaded
|
|
36
|
+
// dynamically once the logger stub is installed above.
|
|
37
|
+
const { createEventHandlerState, dispatchAgentEvent } =
|
|
38
|
+
await import("../conversation-agent-loop-handlers.js");
|
|
39
|
+
|
|
40
|
+
function createDeps(): EventHandlerDeps {
|
|
41
|
+
return {
|
|
42
|
+
ctx: {
|
|
43
|
+
conversationId: "conv-provider-rejection",
|
|
44
|
+
provider: { name: "openai" },
|
|
45
|
+
streamThinking: false,
|
|
46
|
+
emitActivityState: () => {},
|
|
47
|
+
markWorkspaceTopLevelDirty: () => {},
|
|
48
|
+
currentTurnSurfaces: [],
|
|
49
|
+
} as unknown as EventHandlerDeps["ctx"],
|
|
50
|
+
onEvent: (_msg: AssistantEvent) => {},
|
|
51
|
+
reqId: "req-provider-rejection",
|
|
52
|
+
isFirstMessage: false,
|
|
53
|
+
shouldGenerateTitle: false,
|
|
54
|
+
rlog: new Proxy({} as Record<string, unknown>, {
|
|
55
|
+
get: () => () => {},
|
|
56
|
+
}) as unknown as EventHandlerDeps["rlog"],
|
|
57
|
+
turnChannelContext: {
|
|
58
|
+
userMessageChannel: "vellum",
|
|
59
|
+
assistantMessageChannel: "vellum",
|
|
60
|
+
} as EventHandlerDeps["turnChannelContext"],
|
|
61
|
+
turnInterfaceContext: {
|
|
62
|
+
userMessageInterface: "macos",
|
|
63
|
+
assistantMessageInterface: "macos",
|
|
64
|
+
} as EventHandlerDeps["turnInterfaceContext"],
|
|
65
|
+
} as EventHandlerDeps;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("buildProviderRejectionLogFields", () => {
|
|
69
|
+
test("carries the verbatim upstream body", () => {
|
|
70
|
+
// GIVEN a provider rejection that captured the upstream 400 body
|
|
71
|
+
const rawBody = JSON.stringify({
|
|
72
|
+
error: {
|
|
73
|
+
message: "Invalid body: failed to parse JSON value from the request.",
|
|
74
|
+
type: "invalid_request_error",
|
|
75
|
+
code: "invalid_body",
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
const error = new ProviderError(
|
|
79
|
+
"API error (400): Invalid body",
|
|
80
|
+
"openai",
|
|
81
|
+
400,
|
|
82
|
+
{
|
|
83
|
+
rawBody,
|
|
84
|
+
apiErrorCode: "invalid_body",
|
|
85
|
+
apiErrorType: "invalid_request_error",
|
|
86
|
+
requestId: "req_abc123",
|
|
87
|
+
},
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// WHEN diagnostic log fields are built for it
|
|
91
|
+
const fields = buildProviderRejectionLogFields(error);
|
|
92
|
+
|
|
93
|
+
// THEN the body and the upstream metadata ride the record
|
|
94
|
+
expect(fields.upstreamErrorBody).toBe(rawBody);
|
|
95
|
+
expect(fields.upstreamErrorBodyTruncated).toBeUndefined();
|
|
96
|
+
expect(fields.apiErrorCode).toBe("invalid_body");
|
|
97
|
+
expect(fields.apiErrorType).toBe("invalid_request_error");
|
|
98
|
+
expect(fields.requestId).toBe("req_abc123");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("bounds the body to 4 KB and flags the truncation", () => {
|
|
102
|
+
// GIVEN an upstream body larger than the logged ceiling
|
|
103
|
+
const rawBody = "x".repeat(MAX_LOGGED_UPSTREAM_BODY_CHARS + 500);
|
|
104
|
+
const error = new ProviderError("API error (400): huge", "openai", 400, {
|
|
105
|
+
rawBody,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// WHEN diagnostic log fields are built for it
|
|
109
|
+
const fields = buildProviderRejectionLogFields(error);
|
|
110
|
+
|
|
111
|
+
// THEN the logged body stops at the ceiling
|
|
112
|
+
expect(fields.upstreamErrorBody).toHaveLength(
|
|
113
|
+
MAX_LOGGED_UPSTREAM_BODY_CHARS,
|
|
114
|
+
);
|
|
115
|
+
// AND the record says the body was cut
|
|
116
|
+
expect(fields.upstreamErrorBodyTruncated).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("parses the offending content-part coordinates for oversized strings", () => {
|
|
120
|
+
// GIVEN an oversized-content-part rejection naming the request position
|
|
121
|
+
const error = new ProviderError(
|
|
122
|
+
"API error (400): Invalid 'input[191].content[1].text': string too long. Expected a string with maximum length 10485760, but got a string with length 11436754 instead.",
|
|
123
|
+
"openai",
|
|
124
|
+
400,
|
|
125
|
+
{ apiErrorCode: "string_above_max_length" },
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// WHEN diagnostic log fields are built for it
|
|
129
|
+
const fields = buildProviderRejectionLogFields(error);
|
|
130
|
+
|
|
131
|
+
// THEN the message and content indices are logged for a follow-up query
|
|
132
|
+
expect(fields.offendingMessageIndex).toBe(191);
|
|
133
|
+
expect(fields.offendingContentIndex).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("parses coordinates out of the Chat Completions `messages[N]` shape", () => {
|
|
137
|
+
// GIVEN a rejection using the Chat Completions request shape
|
|
138
|
+
const error = new ProviderError(
|
|
139
|
+
"API error (400): Invalid 'messages[7].content[0].text': string too long.",
|
|
140
|
+
"openai",
|
|
141
|
+
400,
|
|
142
|
+
{ apiErrorCode: "string_above_max_length" },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// WHEN diagnostic log fields are built for it
|
|
146
|
+
const fields = buildProviderRejectionLogFields(error);
|
|
147
|
+
|
|
148
|
+
// THEN the same coordinates are extracted
|
|
149
|
+
expect(fields.offendingMessageIndex).toBe(7);
|
|
150
|
+
expect(fields.offendingContentIndex).toBe(0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("leaves coordinates absent for codes that carry no content-part pointer", () => {
|
|
154
|
+
// GIVEN a rejection whose code is outside the narrow tap
|
|
155
|
+
const error = new ProviderError(
|
|
156
|
+
"API error (400): Invalid 'input[3].content[0].text': something else.",
|
|
157
|
+
"openai",
|
|
158
|
+
400,
|
|
159
|
+
{ apiErrorCode: "model_not_found" },
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// WHEN diagnostic log fields are built for it
|
|
163
|
+
const fields = buildProviderRejectionLogFields(error);
|
|
164
|
+
|
|
165
|
+
// THEN no coordinates are inferred from a coincidental match
|
|
166
|
+
expect(fields.offendingMessageIndex).toBeUndefined();
|
|
167
|
+
expect(fields.offendingContentIndex).toBeUndefined();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("scrubs secrets out of the upstream body", () => {
|
|
171
|
+
// GIVEN an upstream body that echoed back a credential
|
|
172
|
+
const projectKey = `sk-proj-${"A".repeat(48)}`;
|
|
173
|
+
const error = new ProviderError("API error (400): bad key", "openai", 400, {
|
|
174
|
+
rawBody: JSON.stringify({
|
|
175
|
+
error: { message: `Incorrect API key provided: ${projectKey}` },
|
|
176
|
+
headers: { authorization: "Bearer some-token-value" },
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// WHEN diagnostic log fields are built for it
|
|
181
|
+
const fields = buildProviderRejectionLogFields(error);
|
|
182
|
+
|
|
183
|
+
// THEN the credential never reaches the log record
|
|
184
|
+
expect(fields.upstreamErrorBody).not.toContain(projectKey);
|
|
185
|
+
expect(fields.upstreamErrorBody).not.toContain("some-token-value");
|
|
186
|
+
expect(fields.upstreamErrorBody).toContain("[REDACTED]");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("reports a null body for errors that are not provider errors", () => {
|
|
190
|
+
// GIVEN a non-provider error
|
|
191
|
+
const error = new Error("socket hang up");
|
|
192
|
+
|
|
193
|
+
// WHEN diagnostic log fields are built for it
|
|
194
|
+
const fields = buildProviderRejectionLogFields(error);
|
|
195
|
+
|
|
196
|
+
// THEN the body is explicitly null rather than missing
|
|
197
|
+
expect(fields.upstreamErrorBody).toBeNull();
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe("unclassified 4xx log line", () => {
|
|
202
|
+
let state: EventHandlerState;
|
|
203
|
+
|
|
204
|
+
beforeEach(() => {
|
|
205
|
+
logRecords.length = 0;
|
|
206
|
+
state = createEventHandlerState();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("includes the upstream response body", async () => {
|
|
210
|
+
// GIVEN an unclassified 4xx provider rejection carrying the upstream body
|
|
211
|
+
const rawBody = JSON.stringify({
|
|
212
|
+
error: {
|
|
213
|
+
message:
|
|
214
|
+
"Invalid body: failed to parse JSON value from the request. Common errors include trailing commas.",
|
|
215
|
+
type: "invalid_request_error",
|
|
216
|
+
code: "invalid_body",
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
const error = new ProviderError(
|
|
220
|
+
"API error (400): Invalid body: failed to parse JSON value from the request.",
|
|
221
|
+
"openai",
|
|
222
|
+
400,
|
|
223
|
+
{ rawBody, apiErrorCode: "invalid_body" },
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// WHEN the agent loop dispatches the error event
|
|
227
|
+
await dispatchAgentEvent(state, createDeps(), { type: "error", error });
|
|
228
|
+
|
|
229
|
+
// THEN the log record carries the body alongside the existing fields
|
|
230
|
+
const record = logRecords.find((r) => r.upstreamErrorBody !== undefined);
|
|
231
|
+
expect(record).toBeDefined();
|
|
232
|
+
expect(record?.upstreamErrorBody).toBe(rawBody);
|
|
233
|
+
expect(record?.conversationId).toBe("conv-provider-rejection");
|
|
234
|
+
expect(record?.statusCode).toBe(400);
|
|
235
|
+
expect(record?.provider).toBe("openai");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("scrubs secrets out of the error message it logs", async () => {
|
|
239
|
+
// GIVEN a rejection whose normalized message echoes a credential
|
|
240
|
+
const error = new ProviderError(
|
|
241
|
+
"API error (400): rejected request with Authorization: Bearer some-token-value",
|
|
242
|
+
"openai",
|
|
243
|
+
400,
|
|
244
|
+
{ rawBody: "{}" },
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// WHEN the agent loop dispatches the error event
|
|
248
|
+
await dispatchAgentEvent(state, createDeps(), { type: "error", error });
|
|
249
|
+
|
|
250
|
+
// THEN the credential is redacted out of the logged message
|
|
251
|
+
const record = logRecords.find((r) => r.upstreamErrorBody !== undefined);
|
|
252
|
+
expect(record?.errorMessage).not.toContain("some-token-value");
|
|
253
|
+
expect(record?.errorMessage).toContain("Bearer [REDACTED]");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("bounds a huge upstream body to 4 KB", async () => {
|
|
257
|
+
// GIVEN an unclassified 4xx rejection whose body exceeds the ceiling
|
|
258
|
+
const error = new ProviderError("API error (400): huge", "openai", 400, {
|
|
259
|
+
rawBody: "y".repeat(MAX_LOGGED_UPSTREAM_BODY_CHARS * 3),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// WHEN the agent loop dispatches the error event
|
|
263
|
+
await dispatchAgentEvent(state, createDeps(), { type: "error", error });
|
|
264
|
+
|
|
265
|
+
// THEN the logged body is capped
|
|
266
|
+
const record = logRecords.find((r) => r.upstreamErrorBody !== undefined);
|
|
267
|
+
expect(record?.upstreamErrorBody).toHaveLength(
|
|
268
|
+
MAX_LOGGED_UPSTREAM_BODY_CHARS,
|
|
269
|
+
);
|
|
270
|
+
expect(record?.upstreamErrorBodyTruncated).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
} from "../usage/pricing.js";
|
|
90
90
|
import { ProviderError } from "../util/errors.js";
|
|
91
91
|
import { faviconUrlForDomain } from "../util/favicon.js";
|
|
92
|
+
import { redactLogString } from "../util/log-redact.js";
|
|
92
93
|
import { getLogger } from "../util/logger.js";
|
|
93
94
|
import { withSqliteRetry } from "../util/sqlite-retry.js";
|
|
94
95
|
import type { DirectiveRequest } from "./assistant-attachments.js";
|
|
@@ -143,6 +144,7 @@ import type {
|
|
|
143
144
|
WebSearchResultItem,
|
|
144
145
|
} from "./message-types/web-activity.js";
|
|
145
146
|
import { referenceMediaBlocksForPersist } from "./persist-media-references.js";
|
|
147
|
+
import { buildProviderRejectionLogFields } from "./provider-rejection-log-fields.js";
|
|
146
148
|
import { turnOrRestingTrust } from "./trust-context-types.js";
|
|
147
149
|
import type { TurnLatencyTracker } from "./turn-latency-tracker.js";
|
|
148
150
|
|
|
@@ -2708,7 +2710,8 @@ function handleError(
|
|
|
2708
2710
|
event.error instanceof ProviderError
|
|
2709
2711
|
? event.error.provider
|
|
2710
2712
|
: undefined,
|
|
2711
|
-
errorMessage: event.error.message,
|
|
2713
|
+
errorMessage: redactLogString(event.error.message),
|
|
2714
|
+
...buildProviderRejectionLogFields(event.error),
|
|
2712
2715
|
},
|
|
2713
2716
|
"Provider rejected request with unclassified 4xx error",
|
|
2714
2717
|
);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isImageDimensionsTooLargeError,
|
|
12
12
|
isImageMediaTypeMismatchError,
|
|
13
13
|
isImageUnprocessableError,
|
|
14
|
+
isImageUnsupportedFormatError,
|
|
14
15
|
} from "../plugins/defaults/image-recovery/detect.js";
|
|
15
16
|
import { ConnectionResolutionError } from "../providers/connection-resolution.js";
|
|
16
17
|
import { PROVIDER_CATALOG } from "../providers/model-catalog.js";
|
|
@@ -539,6 +540,18 @@ function classifyCore(
|
|
|
539
540
|
errorCategory: "image_media_type_mismatch",
|
|
540
541
|
};
|
|
541
542
|
}
|
|
543
|
+
if (isImageUnsupportedFormatError(message)) {
|
|
544
|
+
// Same wire-code reuse as image_unprocessable above. The provider read
|
|
545
|
+
// the bytes and found no image it accepts, so no relabel or resize can
|
|
546
|
+
// rescue the attachment: name the accepted formats instead.
|
|
547
|
+
return {
|
|
548
|
+
code: "IMAGE_TOO_LARGE",
|
|
549
|
+
userMessage:
|
|
550
|
+
"An image in this conversation is not in a format the AI provider accepts (PNG, JPEG, GIF, and WebP are). Remove or convert it and send your message again.",
|
|
551
|
+
retryable: false,
|
|
552
|
+
errorCategory: "image_unsupported_format",
|
|
553
|
+
};
|
|
554
|
+
}
|
|
542
555
|
if (isVisionNotSupportedError(message)) {
|
|
543
556
|
return visionNotSupportedClassification();
|
|
544
557
|
}
|
|
@@ -69,6 +69,7 @@ import type { UserMessageAttachment } from "./message-protocol.js";
|
|
|
69
69
|
import type { ConversationTransportMetadata } from "./message-types/conversations.js";
|
|
70
70
|
import type { TrustContext } from "./trust-context-types.js";
|
|
71
71
|
import { restingTrust } from "./trust-context-types.js";
|
|
72
|
+
import { postUnsendableImageNotice } from "./unsendable-image-notice.js";
|
|
72
73
|
|
|
73
74
|
const log = getLogger("conversation-messaging");
|
|
74
75
|
|
|
@@ -855,7 +856,6 @@ export async function persistQueuedMessageBody(
|
|
|
855
856
|
filePath: attachment.filePath,
|
|
856
857
|
}),
|
|
857
858
|
);
|
|
858
|
-
const cleanMessage = await createUserMessage(content, attachmentInputs);
|
|
859
859
|
let pushedToHistory = false;
|
|
860
860
|
|
|
861
861
|
try {
|
|
@@ -1015,6 +1015,14 @@ export async function persistQueuedMessageBody(
|
|
|
1015
1015
|
attachmentInputs,
|
|
1016
1016
|
);
|
|
1017
1017
|
|
|
1018
|
+
// The turn sees exactly what was persisted: an attachment rejected during
|
|
1019
|
+
// materialization is absent from both, so a file the store refused cannot
|
|
1020
|
+
// reach the model through the in-memory message.
|
|
1021
|
+
const sentAttachments = preparedAttachments.map(
|
|
1022
|
+
(p) => attachmentInputs[p.position],
|
|
1023
|
+
);
|
|
1024
|
+
const cleanMessage = await createUserMessage(content, sentAttachments);
|
|
1025
|
+
|
|
1018
1026
|
// When displayContent is provided (e.g. original text before recording
|
|
1019
1027
|
// intent stripping), persist that to DB so users see the full message
|
|
1020
1028
|
// after restart. The in-memory userMessage (sent to the LLM) still uses
|
|
@@ -1119,7 +1127,7 @@ export async function persistQueuedMessageBody(
|
|
|
1119
1127
|
|
|
1120
1128
|
const llmMessage = enrichMessageWithSourcePaths(
|
|
1121
1129
|
cleanMessage,
|
|
1122
|
-
|
|
1130
|
+
sentAttachments,
|
|
1123
1131
|
);
|
|
1124
1132
|
log.info(
|
|
1125
1133
|
{
|
|
@@ -1144,6 +1152,18 @@ export async function persistQueuedMessageBody(
|
|
|
1144
1152
|
);
|
|
1145
1153
|
}
|
|
1146
1154
|
|
|
1155
|
+
// Read after the content is final (including any link-failure repair), so
|
|
1156
|
+
// the notice describes the blocks the send boundary will actually see.
|
|
1157
|
+
const persistedImages = preparedAttachments.flatMap((p, idx) => {
|
|
1158
|
+
const block = repairedBlocks?.[idx] ?? p.block;
|
|
1159
|
+
if (block.type !== "image") {
|
|
1160
|
+
return [];
|
|
1161
|
+
}
|
|
1162
|
+
const { filename } = attachmentInputs[p.position];
|
|
1163
|
+
return [{ filename, source: block.source }];
|
|
1164
|
+
});
|
|
1165
|
+
await postUnsendableImageNotice(ctx.conversationId, persistedImages);
|
|
1166
|
+
|
|
1147
1167
|
return { id: persistedUserMessage.id, deduplicated: false };
|
|
1148
1168
|
} catch (err) {
|
|
1149
1169
|
if (pushedToHistory) {
|
|
@@ -1827,6 +1827,13 @@ export interface ProcessMessageOptions {
|
|
|
1827
1827
|
displayContent?: string;
|
|
1828
1828
|
/** JWT-verified committer principal for turn-scoped host-proxy authorization. */
|
|
1829
1829
|
sourceActorPrincipalId?: string;
|
|
1830
|
+
/**
|
|
1831
|
+
* The actor this turn is being started for. Stamped onto the conversation
|
|
1832
|
+
* before history is scoped, so the run hydrates as its committer rather
|
|
1833
|
+
* than as whoever last left the resting slot set. Callers with no actor of
|
|
1834
|
+
* their own (internal dispatch) omit it and the resting actor stands.
|
|
1835
|
+
*/
|
|
1836
|
+
trustContext?: TrustContext;
|
|
1830
1837
|
/**
|
|
1831
1838
|
* True when this turn was auto-sent on the user's behalf rather than typed
|
|
1832
1839
|
* (see `PersistMessageOptions.scripted`). Forwarded to persistence so the
|
|
@@ -1873,7 +1880,11 @@ export async function processMessage(
|
|
|
1873
1880
|
sourceActorPrincipalId,
|
|
1874
1881
|
scripted,
|
|
1875
1882
|
metadata: callerMetadata,
|
|
1883
|
+
trustContext: committingTrustContext,
|
|
1876
1884
|
} = options;
|
|
1885
|
+
if (committingTrustContext) {
|
|
1886
|
+
conversation.setTrustContext(committingTrustContext);
|
|
1887
|
+
}
|
|
1877
1888
|
await conversation.ensureActorScopedHistory();
|
|
1878
1889
|
// Snapshot persona context at turn start so later tool turns can't pick up
|
|
1879
1890
|
// a different actor's context if a concurrent request mutates the live fields.
|