@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,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential write path. The single sequence every "store a credential"
|
|
3
|
+
* caller runs.
|
|
4
|
+
*
|
|
5
|
+
* Storing a credential is not one write: the plaintext goes to the secure
|
|
6
|
+
* backend, the value is scrubbed from recent transcripts, the metadata record
|
|
7
|
+
* is upserted, and manual-token providers reconcile their connection row.
|
|
8
|
+
* Order matters (see {@link storeCredentialValue}), so the sequence lives here
|
|
9
|
+
* rather than being re-typed per entry point. The `credentials/set` route and
|
|
10
|
+
* the plugin-facing {@link ../../plugin-api/store-credential.storeCredential}
|
|
11
|
+
* both compose it.
|
|
12
|
+
*
|
|
13
|
+
* The transcript scrub and the connection sync are imported lazily, at the
|
|
14
|
+
* point of use. Both pull heavy graphs behind them (the scrub reaches the
|
|
15
|
+
* conversation database and registry; the sync reaches the OAuth store), and
|
|
16
|
+
* this module sits under `@vellumai/plugin-api`, so a static edge would drag
|
|
17
|
+
* that machinery into every plugin-api consumer for a write most of them never
|
|
18
|
+
* perform.
|
|
19
|
+
*
|
|
20
|
+
* Callers own their own transport-level argument validation and error mapping;
|
|
21
|
+
* this module throws {@link InvalidCredentialInputError} for a value it will
|
|
22
|
+
* not store and {@link CredentialStorageError} when the secure backend write
|
|
23
|
+
* fails.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
ACP_SERVICE,
|
|
28
|
+
assertAcpCredentialFormat,
|
|
29
|
+
} from "../../acp/acp-credentials.js";
|
|
30
|
+
import { credentialKey } from "../../security/credential-key.js";
|
|
31
|
+
import { normalizeSecretValue } from "../../security/secret-normalize.js";
|
|
32
|
+
import {
|
|
33
|
+
getActiveBackendName,
|
|
34
|
+
setSecureKeyAsync,
|
|
35
|
+
} from "../../security/secure-keys.js";
|
|
36
|
+
import { getLogger } from "../../util/logger.js";
|
|
37
|
+
import {
|
|
38
|
+
assertMetadataWritable,
|
|
39
|
+
upsertCredentialMetadata,
|
|
40
|
+
} from "./metadata-store.js";
|
|
41
|
+
import type { CredentialInjectionTemplate } from "./policy-types.js";
|
|
42
|
+
|
|
43
|
+
const log = getLogger("credential-store");
|
|
44
|
+
|
|
45
|
+
/** Raised for a value the store refuses before writing anything. */
|
|
46
|
+
export class InvalidCredentialInputError extends Error {
|
|
47
|
+
constructor(message: string) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "InvalidCredentialInputError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Raised when the secure backend write itself fails. */
|
|
54
|
+
export class CredentialStorageError extends Error {
|
|
55
|
+
constructor(message: string) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "CredentialStorageError";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface StoreCredentialValueInput {
|
|
62
|
+
service: string;
|
|
63
|
+
field: string;
|
|
64
|
+
/** Plaintext value. Edge whitespace is trimmed before storage. */
|
|
65
|
+
value: string;
|
|
66
|
+
/** Human-friendly alias shown in credential listings. */
|
|
67
|
+
alias?: string;
|
|
68
|
+
/** What the credential is for, surfaced to the agent. */
|
|
69
|
+
usageDescription?: string;
|
|
70
|
+
allowedTools?: string[];
|
|
71
|
+
allowedDomains?: string[];
|
|
72
|
+
injectionTemplates?: CredentialInjectionTemplate[];
|
|
73
|
+
/**
|
|
74
|
+
* Skip the retroactive transcript scrub. Set it only when the plaintext
|
|
75
|
+
* provably never transited a conversation (a token minted by an OAuth
|
|
76
|
+
* refresh, say), since the scrub is a bounded but real sweep of recent
|
|
77
|
+
* history.
|
|
78
|
+
*/
|
|
79
|
+
skipTranscriptScrub?: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface StoredCredential {
|
|
83
|
+
credentialId: string;
|
|
84
|
+
service: string;
|
|
85
|
+
field: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Store a credential's plaintext and reconcile everything that hangs off it.
|
|
90
|
+
*
|
|
91
|
+
* The order is load-bearing:
|
|
92
|
+
* 1. Validate the value (and the ACP format guard) so a rejected write leaves
|
|
93
|
+
* no side effects.
|
|
94
|
+
* 2. Fail early when credential metadata is not writable.
|
|
95
|
+
* 3. Write the plaintext to the secure backend.
|
|
96
|
+
* 4. Scrub the value from recent transcripts. This runs BEFORE the metadata
|
|
97
|
+
* upsert and the connection sync, because those can throw and a
|
|
98
|
+
* stored-but-unscrubbed secret must not depend on them succeeding. The
|
|
99
|
+
* credential IS stored at this point, so the scrub is best-effort hygiene
|
|
100
|
+
* and stays invisible to the caller.
|
|
101
|
+
* 5. Upsert metadata and reconcile the manual-token connection row.
|
|
102
|
+
*
|
|
103
|
+
* @throws {InvalidCredentialInputError} when the value is empty or malformed
|
|
104
|
+
* for its service.
|
|
105
|
+
* @throws {CredentialStorageError} when the secure backend rejects the write.
|
|
106
|
+
*/
|
|
107
|
+
export async function storeCredentialValue(
|
|
108
|
+
input: StoreCredentialValueInput,
|
|
109
|
+
): Promise<StoredCredential> {
|
|
110
|
+
const { service, field } = input;
|
|
111
|
+
|
|
112
|
+
const value = normalizeSecretValue(input.value);
|
|
113
|
+
if (value.length === 0) {
|
|
114
|
+
throw new InvalidCredentialInputError("value is required");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Reject an Anthropic API key pasted into the ACP OAuth-token field (a 401
|
|
118
|
+
// footgun) before it reaches storage.
|
|
119
|
+
if (service === ACP_SERVICE) {
|
|
120
|
+
assertAcpCredentialFormat(field, value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
assertMetadataWritable();
|
|
124
|
+
|
|
125
|
+
const stored = await setSecureKeyAsync(credentialKey(service, field), value);
|
|
126
|
+
if (!stored) {
|
|
127
|
+
throw new CredentialStorageError(
|
|
128
|
+
`Failed to store credential in secure storage (backend: ${getActiveBackendName()})`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The stored plaintext may already sit in recent transcripts: the user
|
|
133
|
+
// message that pasted it, the persisted tool_use input, the tool result
|
|
134
|
+
// echoing the command. This is the scrub seam, not setSecureKeyAsync, which
|
|
135
|
+
// also fires on OAuth refresh rotations and MCP header writes whose values
|
|
136
|
+
// never transited a transcript.
|
|
137
|
+
if (!input.skipTranscriptScrub) {
|
|
138
|
+
const { isNonSecretPlatformField, scrubStoredCredentialFromTranscripts } =
|
|
139
|
+
await import("../../daemon/credential-transcript-scrub.js");
|
|
140
|
+
if (!isNonSecretPlatformField(service, field)) {
|
|
141
|
+
try {
|
|
142
|
+
const scrubbed = await scrubStoredCredentialFromTranscripts(value);
|
|
143
|
+
log.info(
|
|
144
|
+
{ service, field, ...scrubbed },
|
|
145
|
+
"Credential stored; scrubbed value from recent transcripts",
|
|
146
|
+
);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
log.warn(
|
|
149
|
+
{ err, service, field },
|
|
150
|
+
"Credential stored, but transcript scrub failed",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const metadata = upsertCredentialMetadata(service, field, {
|
|
157
|
+
alias: input.alias,
|
|
158
|
+
usageDescription: input.usageDescription,
|
|
159
|
+
allowedTools: input.allowedTools,
|
|
160
|
+
allowedDomains: input.allowedDomains,
|
|
161
|
+
injectionTemplates: input.injectionTemplates,
|
|
162
|
+
});
|
|
163
|
+
const { syncManualTokenConnection } =
|
|
164
|
+
await import("../../oauth/manual-token-connection.js");
|
|
165
|
+
await syncManualTokenConnection(service);
|
|
166
|
+
|
|
167
|
+
return { credentialId: metadata.credentialId, service, field };
|
|
168
|
+
}
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* based on the credential's allowed tools list.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { CredentialMetadata } from "./metadata-store.js";
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Canonical capability key for browser-based credential fills.
|
|
10
12
|
*
|
|
@@ -64,3 +66,68 @@ export function isToolAllowed(
|
|
|
64
66
|
(allowed) => resolveCanonical(allowed) === canonical,
|
|
65
67
|
);
|
|
66
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Remediation for a credential whose allowed_tools list is empty. Points at
|
|
72
|
+
* `credentials prompt` (not inline `credentials set`, which agent shells
|
|
73
|
+
* refuse): the secure prompt re-collects the value and sets allowed_tools.
|
|
74
|
+
*/
|
|
75
|
+
const NO_TOOLS_ALLOWED_REMEDIATION =
|
|
76
|
+
"No tools are currently allowed - grant access via `assistant credentials prompt --service <service> --field <field> --label <label> --allowed-tools <tools>` (re-collects the value securely and sets allowed_tools).";
|
|
77
|
+
|
|
78
|
+
/** Denial reason for a tool that is not in a credential's allowed_tools list. */
|
|
79
|
+
export function toolNotAllowedReason(
|
|
80
|
+
toolName: string,
|
|
81
|
+
service: string,
|
|
82
|
+
field: string,
|
|
83
|
+
allowedTools: string[] | undefined,
|
|
84
|
+
): string {
|
|
85
|
+
const tools = allowedTools ?? [];
|
|
86
|
+
return (
|
|
87
|
+
`Tool "${toolName}" is not allowed to use credential ${service}/${field}. ` +
|
|
88
|
+
(tools.length === 0
|
|
89
|
+
? NO_TOOLS_ALLOWED_REMEDIATION
|
|
90
|
+
: `Allowed tools: ${tools.join(", ")}.`)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Evaluate the policy that gates server-side use of a credential.
|
|
96
|
+
*
|
|
97
|
+
* Pure: it inspects only the metadata it is handed, performs no store reads,
|
|
98
|
+
* and has no side effects. Returns the denial reason, or `undefined` when the
|
|
99
|
+
* tool may read the credential server-side.
|
|
100
|
+
*
|
|
101
|
+
* Domain-restricted credentials are scoped to browser fills on those domains,
|
|
102
|
+
* so they are refused here even when the tool itself is allowed.
|
|
103
|
+
*/
|
|
104
|
+
export function serverUseDenialReason(
|
|
105
|
+
metadata: CredentialMetadata | undefined,
|
|
106
|
+
toolName: string,
|
|
107
|
+
service: string,
|
|
108
|
+
field: string,
|
|
109
|
+
): string | undefined {
|
|
110
|
+
if (!metadata) {
|
|
111
|
+
return `No credential found for ${service}/${field}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!isToolAllowed(toolName, metadata.allowedTools)) {
|
|
115
|
+
return toolNotAllowedReason(
|
|
116
|
+
toolName,
|
|
117
|
+
service,
|
|
118
|
+
field,
|
|
119
|
+
metadata.allowedTools,
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const serverDomains = metadata.allowedDomains ?? [];
|
|
124
|
+
if (serverDomains.length > 0) {
|
|
125
|
+
return (
|
|
126
|
+
`Credential ${service}/${field} has domain restrictions ` +
|
|
127
|
+
`(${serverDomains.join(", ")}) and cannot be used server-side. ` +
|
|
128
|
+
"Remove domain restrictions or use a separate credential without domain policy."
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
package/src/tools/executor.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
|
|
3
|
+
import type {
|
|
4
|
+
RiskAllowlistOption,
|
|
5
|
+
RiskDirectoryScopeOption,
|
|
6
|
+
RiskPatternScopeOption,
|
|
7
|
+
} from "@vellumai/gateway-client";
|
|
8
|
+
|
|
3
9
|
import { getConfig } from "../config/loader.js";
|
|
10
|
+
import {
|
|
11
|
+
classifyRisk,
|
|
12
|
+
type RiskClassificationWithMeta,
|
|
13
|
+
} from "../permissions/checker.js";
|
|
4
14
|
import { PermissionPrompter } from "../permissions/prompter.js";
|
|
5
|
-
import {
|
|
6
|
-
|
|
15
|
+
import {
|
|
16
|
+
runInPluginContext,
|
|
17
|
+
runOutsidePluginContext,
|
|
18
|
+
} from "../plugins/plugin-execution-context.js";
|
|
7
19
|
import { TokenExpiredError } from "../security/token-manager.js";
|
|
8
20
|
import {
|
|
9
21
|
recordToolError,
|
|
@@ -30,6 +42,7 @@ import { MAX_FILE_SIZE_BYTES } from "./shared/filesystem/size-guard.js";
|
|
|
30
42
|
import { ToolApprovalHandler } from "./tool-approval-handler.js";
|
|
31
43
|
import { resolveToolInvocationAlias } from "./tool-name-aliases.js";
|
|
32
44
|
import { recordToolCompletion } from "./tool-profiler.js";
|
|
45
|
+
import { RISK_LEVEL_UNCLASSIFIED } from "./tool-types.js";
|
|
33
46
|
import { type ToolContext, type ToolExecutionResult } from "./types.js";
|
|
34
47
|
|
|
35
48
|
export class ToolExecutor {
|
|
@@ -60,18 +73,34 @@ export class ToolExecutor {
|
|
|
60
73
|
): Promise<ToolExecutionResult> {
|
|
61
74
|
const startTime = Date.now();
|
|
62
75
|
let decision = "allow";
|
|
63
|
-
|
|
76
|
+
// The invocation's one classification, taken before the gates so every
|
|
77
|
+
// audit row this call can produce (gate denial, gate error, grant-consumed
|
|
78
|
+
// execution, permission decision) records the risk of what the model asked
|
|
79
|
+
// for, then handed down to `checkPermission`. A classification that does
|
|
80
|
+
// not complete (aborted, gateway unreachable) is recorded as such rather
|
|
81
|
+
// than as a level; the permission check still requires one and fails
|
|
82
|
+
// closed on its own.
|
|
83
|
+
let classification: RiskClassificationWithMeta | undefined;
|
|
84
|
+
try {
|
|
85
|
+
classification = await classifyRisk(
|
|
86
|
+
name,
|
|
87
|
+
input,
|
|
88
|
+
context.workingDir,
|
|
89
|
+
undefined,
|
|
90
|
+
undefined,
|
|
91
|
+
context.signal,
|
|
92
|
+
);
|
|
93
|
+
} catch {
|
|
94
|
+
// Stays undefined; the audit row records RISK_LEVEL_UNCLASSIFIED.
|
|
95
|
+
}
|
|
96
|
+
let riskLevel: string = classification?.level ?? RISK_LEVEL_UNCLASSIFIED;
|
|
64
97
|
let permRiskMeta:
|
|
65
98
|
| {
|
|
66
99
|
riskLevel: string;
|
|
67
100
|
riskReason: string;
|
|
68
|
-
riskScopeOptions:
|
|
69
|
-
riskAllowlistOptions?:
|
|
70
|
-
|
|
71
|
-
description: string;
|
|
72
|
-
pattern: string;
|
|
73
|
-
}>;
|
|
74
|
-
riskDirectoryScopeOptions?: Array<{ scope: string; label: string }>;
|
|
101
|
+
riskScopeOptions: RiskPatternScopeOption[];
|
|
102
|
+
riskAllowlistOptions?: RiskAllowlistOption[];
|
|
103
|
+
riskDirectoryScopeOptions?: RiskDirectoryScopeOption[];
|
|
75
104
|
isContainerized?: boolean;
|
|
76
105
|
}
|
|
77
106
|
| undefined;
|
|
@@ -106,6 +135,11 @@ export class ToolExecutor {
|
|
|
106
135
|
// consumed; substitute the parsed value (with `.catch()` recoveries
|
|
107
136
|
// applied) so validation and execution see the same input.
|
|
108
137
|
if (gateResult.parsedInput) {
|
|
138
|
+
// The permission decision is about the input that runs, so a parse that
|
|
139
|
+
// reshaped it (a `.catch()` recovery) gets its own classification.
|
|
140
|
+
if (!Bun.deepEquals(input, gateResult.parsedInput)) {
|
|
141
|
+
classification = undefined;
|
|
142
|
+
}
|
|
109
143
|
input = gateResult.parsedInput;
|
|
110
144
|
}
|
|
111
145
|
|
|
@@ -189,6 +223,7 @@ export class ToolExecutor {
|
|
|
189
223
|
context,
|
|
190
224
|
startTime,
|
|
191
225
|
computePreviewDiff,
|
|
226
|
+
classification,
|
|
192
227
|
);
|
|
193
228
|
|
|
194
229
|
riskLevel = permResult.riskLevel;
|
|
@@ -235,15 +270,18 @@ export class ToolExecutor {
|
|
|
235
270
|
const execContext = context;
|
|
236
271
|
|
|
237
272
|
// Mark the owning plugin as in context (via AsyncLocalStorage) so host
|
|
238
|
-
// APIs the tool reaches
|
|
273
|
+
// APIs the tool reaches, e.g. resolveCredential, can scope to it. The
|
|
239
274
|
// context must be established around the `execute()` call itself so the
|
|
240
|
-
// returned promise carries the binding across its awaits.
|
|
241
|
-
//
|
|
275
|
+
// returned promise carries the binding across its awaits. A non-plugin
|
|
276
|
+
// tool (default/skill/mcp/workspace) runs with the context explicitly
|
|
277
|
+
// cleared rather than merely unset: the turn may have been started by a
|
|
278
|
+
// plugin (a route handler calling `runConversationTurn`), and this tool
|
|
279
|
+
// is host code that must not inherit that plugin's identity.
|
|
242
280
|
const owner = getToolOwner(name);
|
|
243
281
|
const execPromise =
|
|
244
282
|
owner?.kind === "plugin"
|
|
245
283
|
? runInPluginContext(owner.id, () => tool.execute(input, execContext))
|
|
246
|
-
: tool.execute(input, execContext);
|
|
284
|
+
: runOutsidePluginContext(() => tool.execute(input, execContext));
|
|
247
285
|
|
|
248
286
|
let execResult: ToolExecutionResult = await executeWithTimeout(
|
|
249
287
|
execPromise,
|
|
@@ -295,7 +333,7 @@ export class ToolExecutor {
|
|
|
295
333
|
safeResult.isError,
|
|
296
334
|
);
|
|
297
335
|
|
|
298
|
-
// Merge
|
|
336
|
+
// Merge the classification's risk metadata onto the
|
|
299
337
|
// tool result so downstream consumers (AgentEvent → handleToolResult →
|
|
300
338
|
// ToolResult SSE message) can forward it to the client.
|
|
301
339
|
if (permRiskMeta) {
|
|
@@ -797,7 +797,8 @@ describe("web_search tool", () => {
|
|
|
797
797
|
{
|
|
798
798
|
title: "Keenable Result 1",
|
|
799
799
|
url: "https://example.com/keenable-1",
|
|
800
|
-
description: "
|
|
800
|
+
description: "",
|
|
801
|
+
snippet: "First Keenable result",
|
|
801
802
|
},
|
|
802
803
|
],
|
|
803
804
|
}),
|
|
@@ -814,6 +815,45 @@ describe("web_search tool", () => {
|
|
|
814
815
|
expect(result.content).toContain("https://example.com/keenable-1");
|
|
815
816
|
});
|
|
816
817
|
|
|
818
|
+
test("Keenable reads the page text from snippet, capped", async () => {
|
|
819
|
+
seedWebSearch("your-own", "keenable");
|
|
820
|
+
globalThis.fetch = (async () => {
|
|
821
|
+
return new Response(
|
|
822
|
+
JSON.stringify({
|
|
823
|
+
results: [
|
|
824
|
+
{
|
|
825
|
+
title: "Wrapped",
|
|
826
|
+
url: "https://example.com/wrapped",
|
|
827
|
+
description: "",
|
|
828
|
+
snippet: "line one\n\nline two",
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
title: "Long",
|
|
832
|
+
url: "https://example.com/long",
|
|
833
|
+
description: "",
|
|
834
|
+
snippet: "word ".repeat(400),
|
|
835
|
+
},
|
|
836
|
+
{
|
|
837
|
+
title: "Meta only",
|
|
838
|
+
url: "https://example.com/meta",
|
|
839
|
+
description: "only a meta description",
|
|
840
|
+
},
|
|
841
|
+
],
|
|
842
|
+
}),
|
|
843
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
844
|
+
);
|
|
845
|
+
}) as any;
|
|
846
|
+
|
|
847
|
+
const result = await execute({ query: "test" });
|
|
848
|
+
expect(result.content).toContain("line one line two");
|
|
849
|
+
expect(result.content).toContain("only a meta description");
|
|
850
|
+
const longLine = String(result.content)
|
|
851
|
+
.split("\n")
|
|
852
|
+
.find((line) => line.trim().startsWith("word"));
|
|
853
|
+
expect(longLine?.trim().length).toBeLessThanOrEqual(500);
|
|
854
|
+
expect(longLine?.trim().length).toBeGreaterThan(490);
|
|
855
|
+
});
|
|
856
|
+
|
|
817
857
|
test("Keenable uses the authenticated endpoint when a key is set", async () => {
|
|
818
858
|
seedWebSearch("your-own", "keenable");
|
|
819
859
|
mockKeenableSecureKey = "keen_test";
|
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import { lookup } from "node:dns/promises";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import {
|
|
4
|
+
looksLikeHostPortShorthand,
|
|
5
|
+
looksLikePathOnlyInput,
|
|
6
|
+
} from "@vellumai/service-contracts/url-normalization";
|
|
4
7
|
|
|
5
|
-
export
|
|
6
|
-
if (/^\[[0-9a-fA-F:.%]+\]:\d+(?:[/?#]|$)/.test(value)) {
|
|
7
|
-
return true;
|
|
8
|
-
}
|
|
9
|
-
return /^[^/?#@\s:]+:\d+(?:[/?#]|$)/.test(value);
|
|
10
|
-
}
|
|
8
|
+
export type ResolveHostAddresses = (hostname: string) => Promise<string[]>;
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
value.startsWith("../") ||
|
|
17
|
-
value.startsWith("?") ||
|
|
18
|
-
value.startsWith("#")
|
|
19
|
-
);
|
|
20
|
-
}
|
|
10
|
+
// The URL input-shape predicates are defined once, beside the canonicalizer
|
|
11
|
+
// the gateway builds trust-rule patterns with, so what counts as a `host:port`
|
|
12
|
+
// or a path here is what counts there. Re-exported for existing consumers.
|
|
13
|
+
export { looksLikeHostPortShorthand, looksLikePathOnlyInput };
|
|
21
14
|
|
|
22
15
|
export function parseUrl(input: unknown): URL | null {
|
|
23
16
|
if (typeof input !== "string") {
|
|
@@ -148,7 +148,10 @@ interface FirecrawlSearchResponse {
|
|
|
148
148
|
interface KeenableSearchResult {
|
|
149
149
|
title?: string;
|
|
150
150
|
url?: string;
|
|
151
|
+
/** The page's meta description. Empty for most pages; see keenableSnippet. */
|
|
151
152
|
description?: string;
|
|
153
|
+
/** The page text. This is what carries content on a Keenable result. */
|
|
154
|
+
snippet?: string;
|
|
152
155
|
published_at?: string;
|
|
153
156
|
acquired_at?: string;
|
|
154
157
|
}
|
|
@@ -488,6 +491,20 @@ function firecrawlTbsForFreshness(
|
|
|
488
491
|
}
|
|
489
492
|
}
|
|
490
493
|
|
|
494
|
+
/** Characters of page text kept per result. Keenable returns the whole page on
|
|
495
|
+
* every search result, an order of magnitude more than the other providers here
|
|
496
|
+
* return, so it is capped to keep one search from filling the context. */
|
|
497
|
+
const KEENABLE_SNIPPET_MAX_LENGTH = 500;
|
|
498
|
+
|
|
499
|
+
/** Keenable sends both `snippet` and `description` on every result: `snippet`
|
|
500
|
+
* carries the page text and `description` is the page's meta description, which
|
|
501
|
+
* is empty for most pages. `||` rather than `??`, since the empty description is
|
|
502
|
+
* present rather than nullish. */
|
|
503
|
+
function keenableSnippet(result: KeenableSearchResult): string {
|
|
504
|
+
const text = result.snippet || result.description || "";
|
|
505
|
+
return text.replace(/\s+/g, " ").trim().slice(0, KEENABLE_SNIPPET_MAX_LENGTH);
|
|
506
|
+
}
|
|
507
|
+
|
|
491
508
|
function formatKeenableResults(
|
|
492
509
|
data: KeenableSearchResponse,
|
|
493
510
|
query: string,
|
|
@@ -505,8 +522,9 @@ function formatKeenableResults(
|
|
|
505
522
|
if (r.url) {
|
|
506
523
|
lines.push(` URL: ${r.url}`);
|
|
507
524
|
}
|
|
508
|
-
|
|
509
|
-
|
|
525
|
+
const snippet = keenableSnippet(r);
|
|
526
|
+
if (snippet) {
|
|
527
|
+
lines.push(` ${snippet}`);
|
|
510
528
|
}
|
|
511
529
|
if (r.published_at) {
|
|
512
530
|
lines.push(` Published: ${r.published_at}`);
|
|
@@ -531,7 +549,7 @@ function buildKeenableMetadata(
|
|
|
531
549
|
url,
|
|
532
550
|
domain,
|
|
533
551
|
faviconUrl: faviconUrlForDomain(domain),
|
|
534
|
-
snippet: r
|
|
552
|
+
snippet: keenableSnippet(r),
|
|
535
553
|
};
|
|
536
554
|
});
|
|
537
555
|
return {
|