@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
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared predicates over the raw model-loop {@link ContentBlock} union.
|
|
3
|
+
*
|
|
4
|
+
* The agent loop and the receive-side credential guard judge "did this turn
|
|
5
|
+
* produce anything" the same way, so the predicates live here rather than in
|
|
6
|
+
* each consumer. Plugins keep their own copies: a plugin may not import from
|
|
7
|
+
* outside its own directory.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ContentBlock } from "./types.js";
|
|
11
|
+
|
|
12
|
+
/** Whether `content` carries at least one non-empty `text` block. */
|
|
13
|
+
export function hasVisibleText(content: ReadonlyArray<ContentBlock>): boolean {
|
|
14
|
+
return content.some(
|
|
15
|
+
(block) => block.type === "text" && block.text.trim().length > 0,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Whether `content` carries at least one tool call. */
|
|
20
|
+
export function hasToolUse(content: ReadonlyArray<ContentBlock>): boolean {
|
|
21
|
+
return content.some((block) => block.type === "tool_use");
|
|
22
|
+
}
|
|
@@ -694,8 +694,11 @@ export class GeminiProvider implements Provider {
|
|
|
694
694
|
model: string,
|
|
695
695
|
): Promise<genai.Content[]> {
|
|
696
696
|
// Swap any persisted attachment references back to inline base64 before
|
|
697
|
-
// building parts, so the transforms below can read `source.data`.
|
|
698
|
-
|
|
697
|
+
// building parts, so the transforms below can read `source.data`. Gemini
|
|
698
|
+
// decodes HEIF itself, so an untranscoded HEIC photo stays an image here
|
|
699
|
+
// rather than being replaced by an omission note:
|
|
700
|
+
// https://ai.google.dev/gemini-api/docs/image-understanding
|
|
701
|
+
messages = await resolveMediaReferences(messages, { acceptsHeif: true });
|
|
699
702
|
const result: genai.Content[] = [];
|
|
700
703
|
|
|
701
704
|
// Build a map from tool_use id → function name so tool_result blocks
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
EMPTY_ASSISTANT_TURN_PLACEHOLDER,
|
|
5
|
+
OpenAIChatCompletionsProvider,
|
|
6
|
+
} from "../../openai/chat-completions-provider.js";
|
|
4
7
|
import {
|
|
5
8
|
buildProviderAdapter,
|
|
6
9
|
createAdapterFromConnection,
|
|
@@ -19,6 +22,49 @@ describe("litellm adapter factory", () => {
|
|
|
19
22
|
expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
|
|
20
23
|
});
|
|
21
24
|
|
|
25
|
+
test("backfills placeholder content after an aborted empty assistant turn", async () => {
|
|
26
|
+
const adapter = buildProviderAdapter("litellm", {
|
|
27
|
+
apiKey: "sk-litellm-test",
|
|
28
|
+
model: "deepseek/deepseek-v4",
|
|
29
|
+
streamTimeoutMs: 60_000,
|
|
30
|
+
baseURL: "http://localhost:4000/v1",
|
|
31
|
+
useNativeWebSearch: false,
|
|
32
|
+
});
|
|
33
|
+
expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
|
|
34
|
+
const provider = adapter as OpenAIChatCompletionsProvider;
|
|
35
|
+
const requests: unknown[] = [];
|
|
36
|
+
(provider as unknown as { client: unknown }).client = {
|
|
37
|
+
chat: {
|
|
38
|
+
completions: {
|
|
39
|
+
create: async (params: unknown) => {
|
|
40
|
+
requests.push(params);
|
|
41
|
+
return {
|
|
42
|
+
async *[Symbol.asyncIterator]() {
|
|
43
|
+
yield {
|
|
44
|
+
choices: [
|
|
45
|
+
{ delta: { content: "ok" }, finish_reason: "stop" },
|
|
46
|
+
],
|
|
47
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
await provider.sendMessage([
|
|
57
|
+
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
58
|
+
{ role: "assistant", content: [] },
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const params = requests[0] as {
|
|
62
|
+
messages: Array<{ role: string; content: string | null }>;
|
|
63
|
+
};
|
|
64
|
+
const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
|
|
65
|
+
expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
|
|
66
|
+
});
|
|
67
|
+
|
|
22
68
|
test("buildProviderAdapter works without baseURL (uses proxy default)", () => {
|
|
23
69
|
const adapter = buildProviderAdapter("litellm", {
|
|
24
70
|
apiKey: "sk-litellm-test",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { OllamaProvider } from "../../ollama/client.js";
|
|
4
|
+
import {
|
|
5
|
+
buildProviderAdapter,
|
|
6
|
+
createAdapterFromConnection,
|
|
7
|
+
} from "../adapter-factory.js";
|
|
8
|
+
import type { ProviderConnection, ResolvedAuth } from "../auth.js";
|
|
9
|
+
|
|
10
|
+
interface RetryOptions {
|
|
11
|
+
credentialSource?: string;
|
|
12
|
+
connectionName?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function retryOptions(adapter: unknown): RetryOptions {
|
|
16
|
+
let node = adapter;
|
|
17
|
+
for (let depth = 0; node && depth < 8; depth++) {
|
|
18
|
+
const { options, inner } = node as {
|
|
19
|
+
options?: RetryOptions;
|
|
20
|
+
inner?: unknown;
|
|
21
|
+
};
|
|
22
|
+
if (options) {
|
|
23
|
+
return options;
|
|
24
|
+
}
|
|
25
|
+
node = inner;
|
|
26
|
+
}
|
|
27
|
+
throw new Error("no RetryProvider found in the adapter wrapper chain");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function innermostAdapter(adapter: unknown): unknown {
|
|
31
|
+
let node = adapter;
|
|
32
|
+
for (let depth = 0; node && depth < 8; depth++) {
|
|
33
|
+
const inner = (node as { inner?: unknown }).inner;
|
|
34
|
+
if (!inner) {
|
|
35
|
+
return node;
|
|
36
|
+
}
|
|
37
|
+
node = inner;
|
|
38
|
+
}
|
|
39
|
+
throw new Error("wrapper chain too deep");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("adapter factory ollama", () => {
|
|
43
|
+
test("buildProviderAdapter returns an OllamaProvider", () => {
|
|
44
|
+
const adapter = buildProviderAdapter("ollama", {
|
|
45
|
+
apiKey: "",
|
|
46
|
+
model: "llama3.2",
|
|
47
|
+
streamTimeoutMs: 60_000,
|
|
48
|
+
baseURL: "http://192.168.1.50:11434/v1",
|
|
49
|
+
useNativeWebSearch: false,
|
|
50
|
+
});
|
|
51
|
+
expect(adapter).toBeInstanceOf(OllamaProvider);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("createAdapterFromConnection accepts keyless ollama with a stored baseUrl", () => {
|
|
55
|
+
const connection: ProviderConnection = {
|
|
56
|
+
name: "ollama",
|
|
57
|
+
provider: "ollama",
|
|
58
|
+
auth: { type: "none" },
|
|
59
|
+
label: null,
|
|
60
|
+
baseUrl: "http://192.168.1.50:11434/v1",
|
|
61
|
+
models: null,
|
|
62
|
+
createdAt: Date.now(),
|
|
63
|
+
updatedAt: Date.now(),
|
|
64
|
+
isManaged: false,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const resolvedAuth: ResolvedAuth = {
|
|
68
|
+
kind: "none",
|
|
69
|
+
baseUrl: "http://192.168.1.50:11434/v1",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const adapter = createAdapterFromConnection(connection, resolvedAuth, {
|
|
73
|
+
model: "llama3.2",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(adapter).not.toBeNull();
|
|
77
|
+
expect(innermostAdapter(adapter)).toBeInstanceOf(OllamaProvider);
|
|
78
|
+
expect(retryOptions(adapter)).toMatchObject({
|
|
79
|
+
credentialSource: "no-auth",
|
|
80
|
+
connectionName: "ollama",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -1,12 +1,37 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
EMPTY_ASSISTANT_TURN_PLACEHOLDER,
|
|
5
|
+
OpenAIChatCompletionsProvider,
|
|
6
|
+
} from "../../openai/chat-completions-provider.js";
|
|
4
7
|
import {
|
|
5
8
|
buildProviderAdapter,
|
|
6
9
|
createAdapterFromConnection,
|
|
7
10
|
} from "../adapter-factory.js";
|
|
8
11
|
import type { ProviderConnection, ResolvedAuth } from "../auth.js";
|
|
9
12
|
|
|
13
|
+
function stubChatCreate(provider: OpenAIChatCompletionsProvider): unknown[] {
|
|
14
|
+
const requests: unknown[] = [];
|
|
15
|
+
(provider as unknown as { client: unknown }).client = {
|
|
16
|
+
chat: {
|
|
17
|
+
completions: {
|
|
18
|
+
create: async (params: unknown) => {
|
|
19
|
+
requests.push(params);
|
|
20
|
+
return {
|
|
21
|
+
async *[Symbol.asyncIterator]() {
|
|
22
|
+
yield {
|
|
23
|
+
choices: [{ delta: { content: "ok" }, finish_reason: "stop" }],
|
|
24
|
+
usage: { prompt_tokens: 2, completion_tokens: 1 },
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
return requests;
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
interface RetryOptions {
|
|
11
36
|
credentialSource?: string;
|
|
12
37
|
connectionName?: string;
|
|
@@ -43,6 +68,78 @@ describe("adapter factory", () => {
|
|
|
43
68
|
useNativeWebSearch: false,
|
|
44
69
|
});
|
|
45
70
|
expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
|
|
71
|
+
expect(
|
|
72
|
+
(adapter as unknown as { omitToolChoiceWhenReasoning: boolean })
|
|
73
|
+
.omitToolChoiceWhenReasoning,
|
|
74
|
+
).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("openai-compatible round-trips thinking as reasoning_content", () => {
|
|
78
|
+
const adapter = buildProviderAdapter("openai-compatible", {
|
|
79
|
+
apiKey: "test-key",
|
|
80
|
+
model: "deepseek-reasoner",
|
|
81
|
+
streamTimeoutMs: 60_000,
|
|
82
|
+
baseURL: "https://example.com/v1",
|
|
83
|
+
useNativeWebSearch: false,
|
|
84
|
+
});
|
|
85
|
+
expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
|
|
86
|
+
expect(
|
|
87
|
+
(adapter as unknown as { assistantReasoningField?: string })
|
|
88
|
+
.assistantReasoningField,
|
|
89
|
+
).toBe("reasoning_content");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("litellm round-trips thinking as reasoning_content", () => {
|
|
93
|
+
const adapter = buildProviderAdapter("litellm", {
|
|
94
|
+
apiKey: "test-key",
|
|
95
|
+
model: "deepseek-reasoner",
|
|
96
|
+
streamTimeoutMs: 60_000,
|
|
97
|
+
baseURL: "https://example.com/v1",
|
|
98
|
+
useNativeWebSearch: false,
|
|
99
|
+
});
|
|
100
|
+
expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
|
|
101
|
+
expect(
|
|
102
|
+
(adapter as unknown as { assistantReasoningField?: string })
|
|
103
|
+
.assistantReasoningField,
|
|
104
|
+
).toBe("reasoning_content");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("backfills placeholder content after an aborted empty assistant turn", async () => {
|
|
108
|
+
const adapter = buildProviderAdapter("openai-compatible", {
|
|
109
|
+
apiKey: "test-key",
|
|
110
|
+
model: "deepseek-v4",
|
|
111
|
+
streamTimeoutMs: 60_000,
|
|
112
|
+
baseURL: "http://localhost:8080/v1",
|
|
113
|
+
useNativeWebSearch: false,
|
|
114
|
+
});
|
|
115
|
+
expect(adapter).toBeInstanceOf(OpenAIChatCompletionsProvider);
|
|
116
|
+
const provider = adapter as OpenAIChatCompletionsProvider;
|
|
117
|
+
const requests = stubChatCreate(provider);
|
|
118
|
+
|
|
119
|
+
await provider.sendMessage([
|
|
120
|
+
{ role: "user", content: [{ type: "text", text: "question" }] },
|
|
121
|
+
{
|
|
122
|
+
role: "assistant",
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "thinking",
|
|
126
|
+
thinking: "aborted mid-thought",
|
|
127
|
+
signature: "",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
const params = requests[0] as {
|
|
134
|
+
messages: Array<{
|
|
135
|
+
role: string;
|
|
136
|
+
content: string | null;
|
|
137
|
+
tool_calls?: unknown;
|
|
138
|
+
}>;
|
|
139
|
+
};
|
|
140
|
+
const assistantMsg = params.messages.find((m) => m.role === "assistant")!;
|
|
141
|
+
expect(assistantMsg.content).toBe(EMPTY_ASSISTANT_TURN_PLACEHOLDER);
|
|
142
|
+
expect(assistantMsg.tool_calls).toBeUndefined();
|
|
46
143
|
});
|
|
47
144
|
|
|
48
145
|
test("createAdapterFromConnection wires baseURL from ResolvedAuth", () => {
|
|
@@ -184,6 +184,22 @@ describe("base_url provider-type gate (create)", () => {
|
|
|
184
184
|
);
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
+
test("accepts base_url on ollama provider", async () => {
|
|
188
|
+
mockResolvedAddresses = ["93.184.216.34"];
|
|
189
|
+
const result = await handleCreate({
|
|
190
|
+
body: {
|
|
191
|
+
name: "valid-ollama-remote",
|
|
192
|
+
provider: "ollama",
|
|
193
|
+
auth: { type: "none" },
|
|
194
|
+
base_url: "https://ollama.example.com/v1",
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
expect(result).toBeDefined();
|
|
198
|
+
expect((result as { baseUrl: string }).baseUrl).toBe(
|
|
199
|
+
"https://ollama.example.com/v1",
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
187
203
|
test("allows null base_url on non-openai-compatible provider", async () => {
|
|
188
204
|
// Setting base_url to null is always allowed (it's a no-op clear).
|
|
189
205
|
const result = await handleCreate({
|
|
@@ -433,4 +449,26 @@ describe("base_url provider-type gate (update)", () => {
|
|
|
433
449
|
}),
|
|
434
450
|
).rejects.toThrow(/base_url is only valid for openai-compatible/);
|
|
435
451
|
});
|
|
452
|
+
|
|
453
|
+
test("accepts adding base_url to an existing ollama connection", async () => {
|
|
454
|
+
mockResolvedAddresses = ["93.184.216.34"];
|
|
455
|
+
await handleCreate({
|
|
456
|
+
body: {
|
|
457
|
+
name: "update-test-ollama",
|
|
458
|
+
provider: "ollama",
|
|
459
|
+
auth: { type: "none" },
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const result = await handleUpdate({
|
|
464
|
+
pathParams: { name: "update-test-ollama" },
|
|
465
|
+
body: {
|
|
466
|
+
auth: { type: "none" },
|
|
467
|
+
base_url: "https://ollama.example.com/v1",
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
expect((result as { baseUrl: string }).baseUrl).toBe(
|
|
471
|
+
"https://ollama.example.com/v1",
|
|
472
|
+
);
|
|
473
|
+
});
|
|
436
474
|
});
|
|
@@ -174,6 +174,16 @@ describe("resolveAuth defense-in-depth", () => {
|
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
+
test("preserves baseUrl for ollama none-auth", async () => {
|
|
178
|
+
const result = await resolveAuth({ type: "none" }, "ollama", {
|
|
179
|
+
baseUrl: "http://192.168.1.50:11434/v1",
|
|
180
|
+
});
|
|
181
|
+
expect(result.ok).toBe(true);
|
|
182
|
+
if (result.ok && result.resolved.kind === "none") {
|
|
183
|
+
expect(result.resolved.baseUrl).toBe("http://192.168.1.50:11434/v1");
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
177
187
|
test("handles null baseUrl gracefully for any provider", async () => {
|
|
178
188
|
const result = await resolveAuth(
|
|
179
189
|
{ type: "api_key", credential: "cred-test" },
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
|
|
6
|
+
import { migrateCreateProviderConnections } from "../../../persistence/migrations/243-provider-connections.js";
|
|
7
|
+
import { migrateProviderConnectionStatusLabel } from "../../../persistence/migrations/244-provider-connection-status-label.js";
|
|
8
|
+
import { migrateProviderConnectionBaseUrlAndModels } from "../../../persistence/migrations/250-provider-connection-base-url-and-models.js";
|
|
9
|
+
import * as schema from "../../../persistence/schema/index.js";
|
|
10
|
+
import {
|
|
11
|
+
createConnection,
|
|
12
|
+
getConnection,
|
|
13
|
+
updateConnection,
|
|
14
|
+
} from "../connections.js";
|
|
15
|
+
|
|
16
|
+
function createTestDb() {
|
|
17
|
+
const sqlite = new Database(":memory:");
|
|
18
|
+
sqlite.exec("PRAGMA journal_mode=WAL");
|
|
19
|
+
return drizzle(sqlite, { schema });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bootDb() {
|
|
23
|
+
const db = createTestDb();
|
|
24
|
+
migrateCreateProviderConnections(db);
|
|
25
|
+
migrateProviderConnectionStatusLabel(db);
|
|
26
|
+
migrateProviderConnectionBaseUrlAndModels(db);
|
|
27
|
+
return db;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("ollama connection CRUD", () => {
|
|
31
|
+
test("create without baseUrl leaves it null and does not require models", () => {
|
|
32
|
+
const db = bootDb();
|
|
33
|
+
const result = createConnection(db, {
|
|
34
|
+
name: "ollama-local",
|
|
35
|
+
provider: "ollama",
|
|
36
|
+
auth: { type: "none" },
|
|
37
|
+
});
|
|
38
|
+
expect(result.ok).toBe(true);
|
|
39
|
+
if (result.ok) {
|
|
40
|
+
expect(result.connection.baseUrl).toBeNull();
|
|
41
|
+
expect(result.connection.models).toBeNull();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("create persists an optional baseUrl", () => {
|
|
46
|
+
const db = bootDb();
|
|
47
|
+
const result = createConnection(db, {
|
|
48
|
+
name: "ollama-remote",
|
|
49
|
+
provider: "ollama",
|
|
50
|
+
auth: { type: "none" },
|
|
51
|
+
baseUrl: "http://192.168.1.50:11434/v1",
|
|
52
|
+
});
|
|
53
|
+
expect(result.ok).toBe(true);
|
|
54
|
+
if (result.ok) {
|
|
55
|
+
expect(result.connection.baseUrl).toBe("http://192.168.1.50:11434/v1");
|
|
56
|
+
expect(result.connection.models).toBeNull();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const fetched = getConnection(db, "ollama-remote");
|
|
60
|
+
expect(fetched).not.toBeNull();
|
|
61
|
+
expect(fetched!.baseUrl).toBe("http://192.168.1.50:11434/v1");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("update can change and then clear baseUrl", () => {
|
|
65
|
+
const db = bootDb();
|
|
66
|
+
createConnection(db, {
|
|
67
|
+
name: "ollama-local",
|
|
68
|
+
provider: "ollama",
|
|
69
|
+
auth: { type: "none" },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const updated = updateConnection(db, "ollama-local", {
|
|
73
|
+
auth: { type: "none" },
|
|
74
|
+
baseUrl: "http://127.0.0.1:11434/v1",
|
|
75
|
+
});
|
|
76
|
+
expect(updated.ok).toBe(true);
|
|
77
|
+
if (updated.ok) {
|
|
78
|
+
expect(updated.connection.baseUrl).toBe("http://127.0.0.1:11434/v1");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const cleared = updateConnection(db, "ollama-local", {
|
|
82
|
+
auth: { type: "none" },
|
|
83
|
+
baseUrl: null,
|
|
84
|
+
});
|
|
85
|
+
expect(cleared.ok).toBe(true);
|
|
86
|
+
if (cleared.ok) {
|
|
87
|
+
expect(cleared.connection.baseUrl).toBeNull();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A connection whose credential was force-deleted must fail its next send with
|
|
3
|
+
* an actionable configuration error. Upstreams that answer an unusable key with
|
|
4
|
+
* `200` and empty content otherwise reach the user as a blank assistant turn.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import { classifyConversationError } from "../../../daemon/conversation-error.js";
|
|
10
|
+
import { credentialKey } from "../../../security/credential-key.js";
|
|
11
|
+
import {
|
|
12
|
+
deleteSecureKeyAsync,
|
|
13
|
+
setSecureKeyAsync,
|
|
14
|
+
} from "../../../security/secure-keys.js";
|
|
15
|
+
import { ConnectionResolutionError } from "../../routing-identity.js";
|
|
16
|
+
import type {
|
|
17
|
+
ContentBlock,
|
|
18
|
+
Message,
|
|
19
|
+
Provider,
|
|
20
|
+
ProviderResponse,
|
|
21
|
+
} from "../../types.js";
|
|
22
|
+
import { MissingCredentialGuardProvider } from "../missing-credential-guard.js";
|
|
23
|
+
|
|
24
|
+
const CONNECTION_NAME = "agentrouter-primary";
|
|
25
|
+
const ACCOUNT = credentialKey("agentrouter", "api_key");
|
|
26
|
+
|
|
27
|
+
function respondingProvider(content: ContentBlock[]): Provider {
|
|
28
|
+
return {
|
|
29
|
+
name: "openai-compatible",
|
|
30
|
+
sendMessage: (_messages: Message[]): Promise<ProviderResponse> =>
|
|
31
|
+
Promise.resolve({
|
|
32
|
+
content,
|
|
33
|
+
model: "gpt-4o-mini",
|
|
34
|
+
usage: { inputTokens: 12, outputTokens: 0 },
|
|
35
|
+
stopReason: "end_turn",
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function guard(inner: Provider): Provider {
|
|
41
|
+
return new MissingCredentialGuardProvider(inner, {
|
|
42
|
+
name: CONNECTION_NAME,
|
|
43
|
+
credentialAccount: ACCOUNT,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("missing credential guard", () => {
|
|
48
|
+
test("turns an empty upstream turn into an actionable error once the credential is gone", async () => {
|
|
49
|
+
// GIVEN a connection whose credential has been force-deleted
|
|
50
|
+
await deleteSecureKeyAsync(ACCOUNT);
|
|
51
|
+
|
|
52
|
+
// AND an upstream that answers the unusable key with 200 and no content
|
|
53
|
+
const provider = guard(respondingProvider([]));
|
|
54
|
+
|
|
55
|
+
// WHEN the assistant sends through that connection
|
|
56
|
+
const sending = provider.sendMessage([
|
|
57
|
+
{
|
|
58
|
+
role: "user",
|
|
59
|
+
content: [{ type: "text", text: "посмотри последние ошибки" }],
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
// THEN the send fails as a missing-credential error naming the connection
|
|
64
|
+
const error = await sending.then(
|
|
65
|
+
() => null,
|
|
66
|
+
(err: unknown) => err,
|
|
67
|
+
);
|
|
68
|
+
expect(error).toBeInstanceOf(ConnectionResolutionError);
|
|
69
|
+
const resolution = error as ConnectionResolutionError;
|
|
70
|
+
expect(resolution.reason).toBe("missing_credential");
|
|
71
|
+
expect(resolution.connectionName).toBe(CONNECTION_NAME);
|
|
72
|
+
|
|
73
|
+
// AND the conversation layer renders it as a client-visible error telling
|
|
74
|
+
// the user where to restore the credential, instead of an empty message
|
|
75
|
+
const classified = classifyConversationError(error, {
|
|
76
|
+
phase: "agent_loop",
|
|
77
|
+
});
|
|
78
|
+
expect(classified.userMessage).toContain(CONNECTION_NAME);
|
|
79
|
+
expect(classified.userMessage).toMatch(/credential|API key/i);
|
|
80
|
+
expect(classified.userMessage).toContain("Settings");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("passes an empty turn through while the credential is still stored", async () => {
|
|
84
|
+
// GIVEN a connection whose credential is present
|
|
85
|
+
await setSecureKeyAsync(ACCOUNT, "sk-test-value");
|
|
86
|
+
|
|
87
|
+
// AND an upstream that legitimately returns no content
|
|
88
|
+
const provider = guard(respondingProvider([]));
|
|
89
|
+
|
|
90
|
+
// WHEN the assistant sends through that connection
|
|
91
|
+
const response = await provider.sendMessage([
|
|
92
|
+
{ role: "user", content: [{ type: "text", text: "hello" }] },
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
// THEN the empty response is left alone: it is not a credential problem
|
|
96
|
+
expect(response.content).toEqual([]);
|
|
97
|
+
await deleteSecureKeyAsync(ACCOUNT);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("passes a normal answer through when the credential is gone", async () => {
|
|
101
|
+
// GIVEN a connection with no stored credential
|
|
102
|
+
await deleteSecureKeyAsync(ACCOUNT);
|
|
103
|
+
|
|
104
|
+
// AND an upstream that still answered with text (e.g. a keyless local model)
|
|
105
|
+
const provider = guard(
|
|
106
|
+
respondingProvider([{ type: "text", text: "все хорошо" }]),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// WHEN the assistant sends through that connection
|
|
110
|
+
const response = await provider.sendMessage([
|
|
111
|
+
{ role: "user", content: [{ type: "text", text: "hello" }] },
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
// THEN the answer reaches the user untouched
|
|
115
|
+
expect(response.content).toEqual([{ type: "text", text: "все хорошо" }]);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* 3. Register the client in `ADAPTER_FACTORIES` below.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { normalizeCredentialRef } from "../../security/credential-key.js";
|
|
21
22
|
import { AnthropicProvider } from "../anthropic/client.js";
|
|
22
23
|
import { AtlasCloudProvider } from "../atlascloud/client.js";
|
|
23
24
|
import { BasetenProvider } from "../baseten/client.js";
|
|
@@ -39,6 +40,7 @@ import { VercelAIGatewayProvider } from "../vercel-ai-gateway/client.js";
|
|
|
39
40
|
import type { ResolvedAuth } from "./auth.js";
|
|
40
41
|
import type { ProviderConnection } from "./auth.js";
|
|
41
42
|
import { effectiveConnectionAuth } from "./auth.js";
|
|
43
|
+
import { MissingCredentialGuardProvider } from "./missing-credential-guard.js";
|
|
42
44
|
import { resolveAuth } from "./resolve-auth.js";
|
|
43
45
|
|
|
44
46
|
/** Unified construction opts. Adapters ignore fields they don't consume. */
|
|
@@ -96,12 +98,13 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
|
|
|
96
98
|
// Gemini routes managed proxies through `managedBaseUrl`, not `baseURL`.
|
|
97
99
|
...(baseURL ? { managedBaseUrl: baseURL } : {}),
|
|
98
100
|
}),
|
|
99
|
-
ollama: ({ apiKey, model, streamTimeoutMs }) =>
|
|
101
|
+
ollama: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
|
|
100
102
|
new OllamaProvider(model, {
|
|
101
103
|
// Empty string means keyless — Ollama's client treats undefined as
|
|
102
104
|
// "no key provided" and defaults its internal placeholder.
|
|
103
105
|
apiKey: apiKey || undefined,
|
|
104
106
|
streamTimeoutMs,
|
|
107
|
+
...(baseURL ? { baseURL } : {}),
|
|
105
108
|
}),
|
|
106
109
|
fireworks: ({ apiKey, model, streamTimeoutMs, baseURL }) =>
|
|
107
110
|
new FireworksProvider(apiKey, model, {
|
|
@@ -130,6 +133,13 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
|
|
|
130
133
|
providerName: "litellm",
|
|
131
134
|
providerLabel: "LiteLLM",
|
|
132
135
|
streamTimeoutMs,
|
|
136
|
+
// Replay thinking as `reasoning_content` so DeepSeek-compatible
|
|
137
|
+
// thinking-mode upstreams accept follow-up requests that include tools.
|
|
138
|
+
assistantReasoningField: "reasoning_content",
|
|
139
|
+
// Generic OpenAI-compat proxies may front a strict backend (vLLM,
|
|
140
|
+
// DeepSeek, Portkey). Backfill empty assistant turns so the request
|
|
141
|
+
// satisfies `content or tool_calls must be set`.
|
|
142
|
+
backfillEmptyAssistantContent: true,
|
|
133
143
|
...(baseURL ? { baseURL } : {}),
|
|
134
144
|
}),
|
|
135
145
|
// Keyless openai-compatible endpoints (e.g. LM Studio) ignore the key; the
|
|
@@ -139,6 +149,17 @@ const ADAPTER_FACTORIES: Record<string, AdapterFactory> = {
|
|
|
139
149
|
providerName: "openai-compatible",
|
|
140
150
|
providerLabel: "OpenAI-compatible",
|
|
141
151
|
streamTimeoutMs,
|
|
152
|
+
// Replay thinking as `reasoning_content` so DeepSeek-compatible
|
|
153
|
+
// thinking-mode endpoints accept follow-up requests that include tools.
|
|
154
|
+
assistantReasoningField: "reasoning_content",
|
|
155
|
+
// Custom endpoints (Portkey, vLLM, LM Studio, DeepSeek-compat) often
|
|
156
|
+
// reject `{ role: "assistant", content: null }` after a Stop mid-stream
|
|
157
|
+
// or a reasoning-only turn. Same guard as OpenRouter and Vercel AI
|
|
158
|
+
// Gateway.
|
|
159
|
+
backfillEmptyAssistantContent: true,
|
|
160
|
+
// Custom OpenAI-compatible endpoints may front strict reasoning
|
|
161
|
+
// models (DeepSeek thinking) that 400 on any explicit tool_choice.
|
|
162
|
+
omitToolChoiceWhenReasoning: true,
|
|
142
163
|
...(baseURL ? { baseURL } : {}),
|
|
143
164
|
}),
|
|
144
165
|
minimax: ({ apiKey, model, streamTimeoutMs }) =>
|
|
@@ -270,7 +291,7 @@ export function createAdapterFromConnection(
|
|
|
270
291
|
// identity, the only route that flows through our proxy.
|
|
271
292
|
const isManagedProxy = isVellumManagedConnection(connection);
|
|
272
293
|
const effectiveAuth = effectiveConnectionAuth(connection);
|
|
273
|
-
|
|
294
|
+
const tracked = new UsageTrackingProvider(
|
|
274
295
|
new RetryProvider(adapter, {
|
|
275
296
|
forwardUsageAttributionHeaders: isManagedProxy,
|
|
276
297
|
credentialSource: isManagedProxy
|
|
@@ -288,6 +309,16 @@ export function createAdapterFromConnection(
|
|
|
288
309
|
: {}),
|
|
289
310
|
}),
|
|
290
311
|
);
|
|
312
|
+
|
|
313
|
+
// A credential-backed connection can lose its key between construction and
|
|
314
|
+
// dispatch. Some upstreams answer that with 200 + empty content rather than
|
|
315
|
+
// a 401, which would otherwise land as a blank assistant turn.
|
|
316
|
+
return "credential" in effectiveAuth
|
|
317
|
+
? new MissingCredentialGuardProvider(tracked, {
|
|
318
|
+
name: connection.name,
|
|
319
|
+
credentialAccount: normalizeCredentialRef(effectiveAuth.credential),
|
|
320
|
+
})
|
|
321
|
+
: tracked;
|
|
291
322
|
}
|
|
292
323
|
|
|
293
324
|
function buildConnectionAdapter(
|