@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
|
@@ -44,6 +44,31 @@ export function canActOnPrivilegedDocuments(actor: {
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Whether personal-memory content (memory pages, PKB, matched v3 card
|
|
49
|
+
* sections, the v2 static block) may be surfaced to an actor.
|
|
50
|
+
*
|
|
51
|
+
* The trust class decides, and nothing else. Personal memory is elevated
|
|
52
|
+
* access, and every other consumer of "no actor was bound" already fails
|
|
53
|
+
* closed for it: `FALLBACK_TURN_TRUST` is documented as low-trust and "never
|
|
54
|
+
* guardian", and the workflow resume path deliberately declines to fall back
|
|
55
|
+
* to the guardian context so a resumed run cannot gain trust it did not start
|
|
56
|
+
* with. A gate that granted memory for the same condition would contradict
|
|
57
|
+
* them.
|
|
58
|
+
*
|
|
59
|
+
* Notably this takes no channel. A surface is not an actor: arriving on the
|
|
60
|
+
* first-party console says nothing about who is asking, and an actor whose
|
|
61
|
+
* class could not be resolved fails closed to `unknown` upstream. Answering
|
|
62
|
+
* "who is this" is `resolveTrustClass`'s job -- including the local/native
|
|
63
|
+
* turns that resolve to guardian under a disabled-auth posture -- and this
|
|
64
|
+
* function only answers what that actor may see.
|
|
65
|
+
*/
|
|
66
|
+
export function canSeePersonalMemory(actor: {
|
|
67
|
+
trustClass: RawTrustClass;
|
|
68
|
+
}): boolean {
|
|
69
|
+
return resolveCapabilities(actor.trustClass).canAccessMemory;
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
/**
|
|
48
73
|
* Whether an archive-by-sender invocation is authorized. Any one of a surface
|
|
49
74
|
* action, a task-batch authorization, or an explicit prompt approval suffices.
|
|
@@ -97,8 +97,12 @@ export interface GuardianReplyContext {
|
|
|
97
97
|
channel: string;
|
|
98
98
|
/** Actor identity context for the sender. */
|
|
99
99
|
actor: ActorContext;
|
|
100
|
-
/**
|
|
101
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Conversation the message arrived in (may be the guardian's own). Only the
|
|
102
|
+
* text and NL paths read it; reactions and `apr:` callbacks resolve their
|
|
103
|
+
* target by card address or embedded request id and may pass none.
|
|
104
|
+
*/
|
|
105
|
+
conversationId?: string;
|
|
102
106
|
/** Callback data from button presses (e.g. `apr:<requestId>:<action>`). */
|
|
103
107
|
callbackData?: string;
|
|
104
108
|
/**
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
isDbMigrationGateBypassed,
|
|
28
28
|
} from "../daemon/daemon-readiness.js";
|
|
29
29
|
import { processMessage } from "../daemon/process-message.js";
|
|
30
|
+
import { makeAddrInUseError } from "../daemon/startup-error.js";
|
|
30
31
|
import {
|
|
31
32
|
createLiveVoiceConnection,
|
|
32
33
|
type LiveVoiceConnection,
|
|
@@ -975,9 +976,14 @@ let instance: RuntimeHttpServer | null = null;
|
|
|
975
976
|
|
|
976
977
|
/**
|
|
977
978
|
* Start the runtime HTTP server singleton early in daemon startup so /healthz
|
|
978
|
-
* answers ASAP.
|
|
979
|
-
*
|
|
980
|
-
*
|
|
979
|
+
* answers ASAP.
|
|
980
|
+
*
|
|
981
|
+
* An occupied address (`EADDRINUSE`) aborts startup: whatever holds the port
|
|
982
|
+
* owns every HTTP client of this workspace, including the gateway proxy that
|
|
983
|
+
* fronts `/v1/*`. Continuing would leave a daemon that answers IPC (so `vellum
|
|
984
|
+
* ps` and platform status read healthy) while every proxied HTTP route 502s
|
|
985
|
+
* against a foreign listener. Any other bind failure (permission denied, fd
|
|
986
|
+
* exhaustion) is non-fatal, logged with the singleton left unset.
|
|
981
987
|
*/
|
|
982
988
|
export async function startRuntimeHttpServer(): Promise<void> {
|
|
983
989
|
const port = getRuntimeHttpPort();
|
|
@@ -992,11 +998,20 @@ export async function startRuntimeHttpServer(): Promise<void> {
|
|
|
992
998
|
"Daemon startup: runtime HTTP server listening",
|
|
993
999
|
);
|
|
994
1000
|
} catch (err) {
|
|
1001
|
+
if ((err as NodeJS.ErrnoException).code === "EADDRINUSE") {
|
|
1002
|
+
log.error(
|
|
1003
|
+
{ err, port, hostname },
|
|
1004
|
+
"Runtime HTTP port already in use, aborting startup to avoid an HTTP-blind daemon",
|
|
1005
|
+
);
|
|
1006
|
+
throw makeAddrInUseError(
|
|
1007
|
+
`Runtime HTTP port ${hostname}:${port} is already in use by another process. Stop it, or set RUNTIME_HTTP_PORT to a free port.`,
|
|
1008
|
+
err,
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
995
1011
|
log.warn(
|
|
996
1012
|
{ err, port },
|
|
997
1013
|
"Failed to start runtime HTTP server, continuing without it",
|
|
998
1014
|
);
|
|
999
|
-
instance = null;
|
|
1000
1015
|
}
|
|
1001
1016
|
}
|
|
1002
1017
|
|
|
@@ -1007,7 +1022,7 @@ export async function startRuntimeHttpServer(): Promise<void> {
|
|
|
1007
1022
|
* expiry, profile reaping) must still run; the retry sweep additionally skips
|
|
1008
1023
|
* its cycles while readiness is unready. Never called before migrations settle,
|
|
1009
1024
|
* so the sweeps can't race a schema mid-migration. No-op if the HTTP server
|
|
1010
|
-
*
|
|
1025
|
+
* isn't running or sweeps already started.
|
|
1011
1026
|
*/
|
|
1012
1027
|
export function startRuntimeHttpServerBackgroundSweeps(): void {
|
|
1013
1028
|
instance?.startBackgroundSweeps();
|
|
@@ -333,3 +333,153 @@ describe("report_client_presence route", () => {
|
|
|
333
333
|
).toBeUndefined();
|
|
334
334
|
});
|
|
335
335
|
});
|
|
336
|
+
|
|
337
|
+
describe("report_web_presence route", () => {
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
fakeHttpAuthDisabled = false;
|
|
340
|
+
clearHubClients(assistantEventHub);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
function registerWebClient(args: {
|
|
344
|
+
clientId: string;
|
|
345
|
+
actorPrincipalId?: string;
|
|
346
|
+
}): void {
|
|
347
|
+
registerHubClient({
|
|
348
|
+
hub: assistantEventHub,
|
|
349
|
+
clientId: args.clientId,
|
|
350
|
+
interfaceId: "web",
|
|
351
|
+
actorPrincipalId: args.actorPrincipalId,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
test("records visibility and focused conversation against the client named by the header", () => {
|
|
356
|
+
registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
357
|
+
|
|
358
|
+
const handler = findHandler("report_web_presence");
|
|
359
|
+
const result = handler({
|
|
360
|
+
headers: {
|
|
361
|
+
"x-vellum-client-id": "client-A1",
|
|
362
|
+
"x-vellum-actor-principal-id": "user-A",
|
|
363
|
+
},
|
|
364
|
+
body: { visible: true, focusedConversationId: "conv-1" },
|
|
365
|
+
}) as { recorded: boolean };
|
|
366
|
+
|
|
367
|
+
expect(result).toEqual({ recorded: true });
|
|
368
|
+
expect(
|
|
369
|
+
assistantEventHub.getClientById("client-A1")?.webPresence,
|
|
370
|
+
).toMatchObject({
|
|
371
|
+
visible: true,
|
|
372
|
+
focusedConversationId: "conv-1",
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("accepts a null focusedConversationId", () => {
|
|
377
|
+
registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
378
|
+
|
|
379
|
+
const handler = findHandler("report_web_presence");
|
|
380
|
+
const result = handler({
|
|
381
|
+
headers: {
|
|
382
|
+
"x-vellum-client-id": "client-A1",
|
|
383
|
+
"x-vellum-actor-principal-id": "user-A",
|
|
384
|
+
},
|
|
385
|
+
body: { visible: false, focusedConversationId: null },
|
|
386
|
+
}) as { recorded: boolean };
|
|
387
|
+
|
|
388
|
+
expect(result).toEqual({ recorded: true });
|
|
389
|
+
expect(
|
|
390
|
+
assistantEventHub.getClientById("client-A1")?.webPresence,
|
|
391
|
+
).toMatchObject({
|
|
392
|
+
visible: false,
|
|
393
|
+
focusedConversationId: null,
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("returns recorded false when the reported client is not connected", () => {
|
|
398
|
+
const handler = findHandler("report_web_presence");
|
|
399
|
+
const result = handler({
|
|
400
|
+
headers: {
|
|
401
|
+
"x-vellum-client-id": "client-gone",
|
|
402
|
+
"x-vellum-actor-principal-id": "user-A",
|
|
403
|
+
},
|
|
404
|
+
body: { visible: true, focusedConversationId: "conv-1" },
|
|
405
|
+
}) as { recorded: boolean };
|
|
406
|
+
|
|
407
|
+
expect(result).toEqual({ recorded: false });
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("returns recorded false when the caller does not own the client", () => {
|
|
411
|
+
registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
412
|
+
|
|
413
|
+
const handler = findHandler("report_web_presence");
|
|
414
|
+
const result = handler({
|
|
415
|
+
headers: {
|
|
416
|
+
"x-vellum-client-id": "client-A1",
|
|
417
|
+
"x-vellum-actor-principal-id": "user-B",
|
|
418
|
+
},
|
|
419
|
+
body: { visible: true, focusedConversationId: "conv-1" },
|
|
420
|
+
}) as { recorded: boolean };
|
|
421
|
+
|
|
422
|
+
expect(result).toEqual({ recorded: false });
|
|
423
|
+
expect(
|
|
424
|
+
assistantEventHub.getClientById("client-A1")?.webPresence,
|
|
425
|
+
).toBeUndefined();
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("returns recorded false for a client with no stored actorPrincipalId", () => {
|
|
429
|
+
registerWebClient({ clientId: "client-noprincipal" });
|
|
430
|
+
|
|
431
|
+
const handler = findHandler("report_web_presence");
|
|
432
|
+
const result = handler({
|
|
433
|
+
headers: {
|
|
434
|
+
"x-vellum-client-id": "client-noprincipal",
|
|
435
|
+
"x-vellum-actor-principal-id": "user-A",
|
|
436
|
+
},
|
|
437
|
+
body: { visible: true, focusedConversationId: "conv-1" },
|
|
438
|
+
}) as { recorded: boolean };
|
|
439
|
+
|
|
440
|
+
expect(result).toEqual({ recorded: false });
|
|
441
|
+
expect(
|
|
442
|
+
assistantEventHub.getClientById("client-noprincipal")?.webPresence,
|
|
443
|
+
).toBeUndefined();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("dev-bypass mode records presence without an ownership check", () => {
|
|
447
|
+
fakeHttpAuthDisabled = true;
|
|
448
|
+
registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
449
|
+
|
|
450
|
+
const handler = findHandler("report_web_presence");
|
|
451
|
+
const result = handler({
|
|
452
|
+
headers: {
|
|
453
|
+
"x-vellum-client-id": "client-A1",
|
|
454
|
+
"x-vellum-actor-principal-id": "user-B",
|
|
455
|
+
},
|
|
456
|
+
body: { visible: true, focusedConversationId: "conv-1" },
|
|
457
|
+
}) as { recorded: boolean };
|
|
458
|
+
|
|
459
|
+
expect(result).toEqual({ recorded: true });
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("throws BadRequestError when the client-id header is missing", () => {
|
|
463
|
+
const handler = findHandler("report_web_presence");
|
|
464
|
+
|
|
465
|
+
expect(() =>
|
|
466
|
+
handler({ body: { visible: true, focusedConversationId: "conv-1" } }),
|
|
467
|
+
).toThrow(BadRequestError);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
test("throws BadRequestError for a malformed body", () => {
|
|
471
|
+
registerWebClient({ clientId: "client-A1", actorPrincipalId: "user-A" });
|
|
472
|
+
|
|
473
|
+
const handler = findHandler("report_web_presence");
|
|
474
|
+
|
|
475
|
+
expect(() =>
|
|
476
|
+
handler({
|
|
477
|
+
headers: {
|
|
478
|
+
"x-vellum-client-id": "client-A1",
|
|
479
|
+
"x-vellum-actor-principal-id": "user-A",
|
|
480
|
+
},
|
|
481
|
+
body: { visible: "yes", focusedConversationId: "conv-1" },
|
|
482
|
+
}),
|
|
483
|
+
).toThrow(BadRequestError);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
@@ -683,6 +683,139 @@ describe("GET /v1/conversations with needsAttention", () => {
|
|
|
683
683
|
});
|
|
684
684
|
});
|
|
685
685
|
|
|
686
|
+
describe("GET /v1/conversations with foregroundOnly", () => {
|
|
687
|
+
function fileIntoGroup(conversationId: string, groupId: string): void {
|
|
688
|
+
rawRun(
|
|
689
|
+
"test:fileIntoGroup",
|
|
690
|
+
"UPDATE conversations SET group_id = ? WHERE id = ?",
|
|
691
|
+
groupId,
|
|
692
|
+
conversationId,
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function surface(conversationId: string): void {
|
|
697
|
+
rawRun(
|
|
698
|
+
"test:surfaceConversation",
|
|
699
|
+
"UPDATE conversations SET surfaced_at = ? WHERE id = ?",
|
|
700
|
+
Date.now(),
|
|
701
|
+
conversationId,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
beforeEach(() => {
|
|
706
|
+
clearConversations();
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
test("drops the automated runs the standard listing admits through a custom group", async () => {
|
|
710
|
+
/* The standard listing shows a background run filed in a custom group,
|
|
711
|
+
so a caller reading page one to find "the newest chat" can lead with
|
|
712
|
+
one; the filter is what lets that caller ask for chats alone. A
|
|
713
|
+
surfaced run is a chat for this purpose, exactly as the unread count
|
|
714
|
+
treats it. */
|
|
715
|
+
const group = createGroup("Work");
|
|
716
|
+
createConversation("plain-chat");
|
|
717
|
+
const bgInGroup = createConversation({
|
|
718
|
+
title: "bg-in-group",
|
|
719
|
+
conversationType: "background",
|
|
720
|
+
});
|
|
721
|
+
fileIntoGroup(bgInGroup.id, group.id);
|
|
722
|
+
const surfacedBg = createConversation({
|
|
723
|
+
title: "surfaced-bg",
|
|
724
|
+
conversationType: "background",
|
|
725
|
+
});
|
|
726
|
+
surface(surfacedBg.id);
|
|
727
|
+
|
|
728
|
+
const unfiltered = await invoke();
|
|
729
|
+
expect(unfiltered.conversations.map((c) => c.title).sort()).toEqual([
|
|
730
|
+
"bg-in-group",
|
|
731
|
+
"plain-chat",
|
|
732
|
+
"surfaced-bg",
|
|
733
|
+
]);
|
|
734
|
+
|
|
735
|
+
const result = await invoke({ foregroundOnly: "true" });
|
|
736
|
+
expect(result.conversations.map((c) => c.title).sort()).toEqual([
|
|
737
|
+
"plain-chat",
|
|
738
|
+
"surfaced-bg",
|
|
739
|
+
]);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
test("hasMore and the total describe the filtered set, not the whole table", async () => {
|
|
743
|
+
/* The reason for the filter is a limit=1 read whose first row is the
|
|
744
|
+
answer; that read is only right if the page and its total come from
|
|
745
|
+
the same predicate. */
|
|
746
|
+
const group = createGroup("Work");
|
|
747
|
+
for (let i = 0; i < 3; i++) {
|
|
748
|
+
const bg = createConversation({
|
|
749
|
+
title: `bg-${i}`,
|
|
750
|
+
conversationType: "background",
|
|
751
|
+
});
|
|
752
|
+
fileIntoGroup(bg.id, group.id);
|
|
753
|
+
}
|
|
754
|
+
createConversation("the-only-chat");
|
|
755
|
+
|
|
756
|
+
const page = await invoke({ foregroundOnly: "true", limit: "1" });
|
|
757
|
+
|
|
758
|
+
expect(page.conversations.map((c) => c.title)).toEqual(["the-only-chat"]);
|
|
759
|
+
expect(page.hasMore).toBe(false);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
test("a foreground-only first page has no pinned rows appended to it", async () => {
|
|
763
|
+
/* Same rule as the attention- and group-scoped pages: the injection is
|
|
764
|
+
for a client reading Pinned out of the unfiltered list, and a caller
|
|
765
|
+
that asked for the newest chat in one row is not that client. The
|
|
766
|
+
older pinned chat is a member of the filter, so only the injection
|
|
767
|
+
could put it on a limit=1 page. */
|
|
768
|
+
const older = createConversation("older-pinned-chat");
|
|
769
|
+
rawRun(
|
|
770
|
+
"test:pinConversation",
|
|
771
|
+
"UPDATE conversations SET is_pinned = 1, group_id = 'system:pinned', last_message_at = ? WHERE id = ?",
|
|
772
|
+
Date.now() - 60_000,
|
|
773
|
+
older.id,
|
|
774
|
+
);
|
|
775
|
+
const newer = createConversation("newest-chat");
|
|
776
|
+
rawRun(
|
|
777
|
+
"test:bumpRecency",
|
|
778
|
+
"UPDATE conversations SET last_message_at = ? WHERE id = ?",
|
|
779
|
+
Date.now(),
|
|
780
|
+
newer.id,
|
|
781
|
+
);
|
|
782
|
+
|
|
783
|
+
const unfiltered = await invoke({ limit: "1" });
|
|
784
|
+
expect(unfiltered.conversations.map((c) => c.title)).toEqual([
|
|
785
|
+
"newest-chat",
|
|
786
|
+
"older-pinned-chat",
|
|
787
|
+
]);
|
|
788
|
+
|
|
789
|
+
const result = await invoke({ foregroundOnly: "true", limit: "1" });
|
|
790
|
+
expect(result.conversations.map((c) => c.title)).toEqual(["newest-chat"]);
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test("composes with the other filters", async () => {
|
|
794
|
+
const group = createGroup("Work");
|
|
795
|
+
const chatInGroup = createConversation("chat-in-group");
|
|
796
|
+
fileIntoGroup(chatInGroup.id, group.id);
|
|
797
|
+
const bgInGroup = createConversation({
|
|
798
|
+
title: "bg-in-group",
|
|
799
|
+
conversationType: "background",
|
|
800
|
+
});
|
|
801
|
+
fileIntoGroup(bgInGroup.id, group.id);
|
|
802
|
+
createConversation("chat-outside");
|
|
803
|
+
|
|
804
|
+
const result = await invoke({
|
|
805
|
+
foregroundOnly: "true",
|
|
806
|
+
groupId: group.id,
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
expect(result.conversations.map((c) => c.title)).toEqual(["chat-in-group"]);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test('any value other than "true" is rejected with a 400', () => {
|
|
813
|
+
for (const bad of ["false", "1", "yes", "TRUE"]) {
|
|
814
|
+
expect(() => invoke({ foregroundOnly: bad })).toThrow(BadRequestError);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
});
|
|
818
|
+
|
|
686
819
|
describe("GET /v1/conversations/unread-count", () => {
|
|
687
820
|
const unreadCountHandler = findHandler(
|
|
688
821
|
CONVERSATION_LIST_ROUTES,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deleting a credential an LLM connection dispatches with must be refused
|
|
3
|
+
* until the caller confirms, because losing it silently takes that connection
|
|
4
|
+
* offline: the adapter keeps routing to the provider with no key and the turn
|
|
5
|
+
* comes back empty.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
import { getDb } from "../../../persistence/db-connection.js";
|
|
11
|
+
import { initializeDb } from "../../../persistence/db-init.js";
|
|
12
|
+
import { providerConnections } from "../../../persistence/schema/inference.js";
|
|
13
|
+
import { createConnection } from "../../../providers/inference/connections.js";
|
|
14
|
+
import { credentialKey } from "../../../security/credential-key.js";
|
|
15
|
+
import {
|
|
16
|
+
deleteSecureKeyAsync,
|
|
17
|
+
getSecureKeyAsync,
|
|
18
|
+
setSecureKeyAsync,
|
|
19
|
+
} from "../../../security/secure-keys.js";
|
|
20
|
+
import { CredentialInUseError } from "../credential-in-use.js";
|
|
21
|
+
import { ROUTES as CREDENTIAL_ROUTES } from "../credential-routes.js";
|
|
22
|
+
import { ROUTES as SECRET_ROUTES } from "../secret-routes.js";
|
|
23
|
+
import type { RouteDefinition } from "../types.js";
|
|
24
|
+
|
|
25
|
+
await initializeDb();
|
|
26
|
+
|
|
27
|
+
const SERVICE = "agentrouter";
|
|
28
|
+
const FIELD = "api_key";
|
|
29
|
+
const ACCOUNT = credentialKey(SERVICE, FIELD);
|
|
30
|
+
|
|
31
|
+
function findHandler(
|
|
32
|
+
routes: RouteDefinition[],
|
|
33
|
+
operationId: string,
|
|
34
|
+
): RouteDefinition["handler"] {
|
|
35
|
+
const route = routes.find(
|
|
36
|
+
(candidate) => candidate.operationId === operationId,
|
|
37
|
+
);
|
|
38
|
+
if (!route) {
|
|
39
|
+
throw new Error(`Route ${operationId} not found`);
|
|
40
|
+
}
|
|
41
|
+
return route.handler;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const deleteCredential = findHandler(CREDENTIAL_ROUTES, "credentials_delete");
|
|
45
|
+
const deleteSecret = findHandler(SECRET_ROUTES, "secrets_delete");
|
|
46
|
+
|
|
47
|
+
/** Run a handler and hand back whatever it rejected with, or null on success. */
|
|
48
|
+
async function rejection(run: () => unknown): Promise<unknown> {
|
|
49
|
+
try {
|
|
50
|
+
await run();
|
|
51
|
+
return null;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Point an api_key connection at the credential under test. */
|
|
58
|
+
function createConnectionUsing(name: string, credential: string): void {
|
|
59
|
+
const result = createConnection(getDb(), {
|
|
60
|
+
name,
|
|
61
|
+
provider: "openai",
|
|
62
|
+
auth: { type: "api_key", credential },
|
|
63
|
+
});
|
|
64
|
+
if (!result.ok) {
|
|
65
|
+
throw new Error(`Failed to create connection ${name}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
beforeEach(async () => {
|
|
70
|
+
getDb().delete(providerConnections).run();
|
|
71
|
+
await deleteSecureKeyAsync(ACCOUNT);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("credential delete in-use guard", () => {
|
|
75
|
+
test("refuses a delete while a connection resolves auth through the credential", async () => {
|
|
76
|
+
// GIVEN a stored credential
|
|
77
|
+
await setSecureKeyAsync(ACCOUNT, "sk-test-value");
|
|
78
|
+
|
|
79
|
+
// AND two LLM connections that dispatch with it
|
|
80
|
+
createConnectionUsing("router-primary", `${SERVICE}:${FIELD}`);
|
|
81
|
+
createConnectionUsing("router-fallback", ACCOUNT);
|
|
82
|
+
|
|
83
|
+
// WHEN the credential is deleted without confirmation
|
|
84
|
+
const error = await rejection(() =>
|
|
85
|
+
deleteCredential({ body: { service: SERVICE, field: FIELD } }),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// THEN the delete is refused as a client error naming the connections
|
|
89
|
+
expect(error).toBeInstanceOf(CredentialInUseError);
|
|
90
|
+
const refusal = error as CredentialInUseError;
|
|
91
|
+
expect(refusal.statusCode).toBe(400);
|
|
92
|
+
expect(refusal.code).toBe("CREDENTIAL_IN_USE");
|
|
93
|
+
expect(refusal.connections).toEqual(["router-fallback", "router-primary"]);
|
|
94
|
+
expect(refusal.message).toContain("router-primary");
|
|
95
|
+
|
|
96
|
+
// AND the credential is left in place, so those connections keep working
|
|
97
|
+
expect(await getSecureKeyAsync(ACCOUNT)).toBe("sk-test-value");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("deletes the credential when the caller confirms with force", async () => {
|
|
101
|
+
// GIVEN a stored credential a connection dispatches with
|
|
102
|
+
await setSecureKeyAsync(ACCOUNT, "sk-test-value");
|
|
103
|
+
createConnectionUsing("router-primary", `${SERVICE}:${FIELD}`);
|
|
104
|
+
|
|
105
|
+
// WHEN the delete is retried with force
|
|
106
|
+
const result = await deleteCredential({
|
|
107
|
+
body: { service: SERVICE, field: FIELD, force: true },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// THEN it succeeds and reports the connections it took offline
|
|
111
|
+
expect(result).toMatchObject({
|
|
112
|
+
service: SERVICE,
|
|
113
|
+
field: FIELD,
|
|
114
|
+
affectedConnections: ["router-primary"],
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// AND the credential is gone from secure storage
|
|
118
|
+
expect(await getSecureKeyAsync(ACCOUNT)).toBeUndefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("deletes an unreferenced credential without confirmation", async () => {
|
|
122
|
+
// GIVEN a stored credential no connection references
|
|
123
|
+
await setSecureKeyAsync(ACCOUNT, "sk-test-value");
|
|
124
|
+
createConnectionUsing("other-connection", "credential/openai/api_key");
|
|
125
|
+
|
|
126
|
+
// WHEN it is deleted without force
|
|
127
|
+
const result = await deleteCredential({
|
|
128
|
+
body: { service: SERVICE, field: FIELD },
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// THEN the delete goes through with no affected connections
|
|
132
|
+
expect(result).toMatchObject({ affectedConnections: [] });
|
|
133
|
+
expect(await getSecureKeyAsync(ACCOUNT)).toBeUndefined();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("guards the api-key delete surface too", async () => {
|
|
137
|
+
// GIVEN a provider API key a connection dispatches with
|
|
138
|
+
const apiKeyAccount = credentialKey("openrouter", FIELD);
|
|
139
|
+
await setSecureKeyAsync(apiKeyAccount, "sk-test-value");
|
|
140
|
+
createConnectionUsing("router-primary", apiKeyAccount);
|
|
141
|
+
|
|
142
|
+
// WHEN it is deleted through the secrets route without confirmation
|
|
143
|
+
const error = await rejection(() =>
|
|
144
|
+
deleteSecret({ body: { type: "api_key", name: "openrouter" } }),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// THEN the same refusal applies and the key survives
|
|
148
|
+
expect(error).toBeInstanceOf(CredentialInUseError);
|
|
149
|
+
expect((error as CredentialInUseError).connections).toEqual([
|
|
150
|
+
"router-primary",
|
|
151
|
+
]);
|
|
152
|
+
expect(await getSecureKeyAsync(apiKeyAccount)).toBe("sk-test-value");
|
|
153
|
+
await deleteSecureKeyAsync(apiKeyAccount);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -313,8 +313,59 @@ describe("POST inference/provider-connections (create)", () => {
|
|
|
313
313
|
auth: { type: "none" },
|
|
314
314
|
},
|
|
315
315
|
},
|
|
316
|
-
)) as { auth: object };
|
|
316
|
+
)) as { auth: object; baseUrl: string | null };
|
|
317
|
+
expect(result.auth).toEqual({ type: "none" });
|
|
318
|
+
expect(result.baseUrl).toBeNull();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("creates an ollama connection with an optional base_url", async () => {
|
|
322
|
+
const result = (await call(
|
|
323
|
+
findHandler("inference_provider_connections_create"),
|
|
324
|
+
{
|
|
325
|
+
body: {
|
|
326
|
+
name: "ollama-remote",
|
|
327
|
+
provider: "ollama",
|
|
328
|
+
auth: { type: "none" },
|
|
329
|
+
base_url: "http://192.168.1.50:11434/v1",
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
)) as { auth: object; baseUrl: string | null };
|
|
317
333
|
expect(result.auth).toEqual({ type: "none" });
|
|
334
|
+
expect(result.baseUrl).toBe("http://192.168.1.50:11434/v1");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("updates an ollama connection base_url and can clear it", async () => {
|
|
338
|
+
await call(findHandler("inference_provider_connections_create"), {
|
|
339
|
+
body: {
|
|
340
|
+
name: "ollama-editable",
|
|
341
|
+
provider: "ollama",
|
|
342
|
+
auth: { type: "none" },
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const updated = (await call(
|
|
347
|
+
findHandler("inference_provider_connections_update"),
|
|
348
|
+
{
|
|
349
|
+
pathParams: { name: "ollama-editable" },
|
|
350
|
+
body: {
|
|
351
|
+
auth: { type: "none" },
|
|
352
|
+
base_url: "http://127.0.0.1:11434/v1",
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
)) as { baseUrl: string | null };
|
|
356
|
+
expect(updated.baseUrl).toBe("http://127.0.0.1:11434/v1");
|
|
357
|
+
|
|
358
|
+
const cleared = (await call(
|
|
359
|
+
findHandler("inference_provider_connections_update"),
|
|
360
|
+
{
|
|
361
|
+
pathParams: { name: "ollama-editable" },
|
|
362
|
+
body: {
|
|
363
|
+
auth: { type: "none" },
|
|
364
|
+
base_url: null,
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
)) as { baseUrl: string | null };
|
|
368
|
+
expect(cleared.baseUrl).toBeNull();
|
|
318
369
|
});
|
|
319
370
|
|
|
320
371
|
test("derives api_key auth from provider + credential when auth is omitted", async () => {
|
|
@@ -148,6 +148,7 @@ describe("monitoring_status", () => {
|
|
|
148
148
|
memoryStat: {
|
|
149
149
|
anonBytes: 4 * 1024 * 1024 * 1024,
|
|
150
150
|
fileBytes: 1024 * 1024 * 1024,
|
|
151
|
+
inactiveFileBytes: 768 * 1024 * 1024,
|
|
151
152
|
kernelBytes: 512 * 1024 * 1024,
|
|
152
153
|
slabReclaimableBytes: 400 * 1024 * 1024,
|
|
153
154
|
slabUnreclaimableBytes: 100 * 1024 * 1024,
|