@vellumai/assistant 0.6.2 → 0.6.3
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/bun.lock +40 -40
- package/bunfig.toml +3 -0
- package/docs/architecture/memory.md +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
- package/openapi.yaml +184 -69
- package/package.json +41 -41
- package/scripts/generate-openapi.ts +1 -2
- package/src/__tests__/acp-session.test.ts +43 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +1 -0
- package/src/__tests__/app-source-watcher.test.ts +37 -11
- package/src/__tests__/approval-routes-http.test.ts +178 -1
- package/src/__tests__/browser-fill-credential.test.ts +229 -94
- package/src/__tests__/browser-manager.test.ts +40 -27
- package/src/__tests__/catalog-files.test.ts +862 -0
- package/src/__tests__/channel-approvals.test.ts +53 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +125 -48
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
- package/src/__tests__/context-overflow-approval.test.ts +16 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +1 -1
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
- package/src/__tests__/conversation-fork-crud.test.ts +17 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
- package/src/__tests__/conversation-inject-context.test.ts +103 -0
- package/src/__tests__/conversation-queue.test.ts +45 -2
- package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
- package/src/__tests__/conversation-starter-routes.test.ts +126 -0
- package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
- package/src/__tests__/conversation-store.test.ts +195 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -4
- package/src/__tests__/credential-vault.test.ts +152 -13
- package/src/__tests__/credentials-cli.test.ts +2 -2
- package/src/__tests__/date-context.test.ts +4 -4
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
- package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/gemini-provider.test.ts +2 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
- package/src/__tests__/headless-browser-interactions.test.ts +707 -371
- package/src/__tests__/headless-browser-navigate.test.ts +389 -47
- package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
- package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
- package/src/__tests__/host-bash-proxy.test.ts +150 -1
- package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
- package/src/__tests__/host-browser-event-routes.test.ts +350 -0
- package/src/__tests__/host-browser-proxy.test.ts +444 -0
- package/src/__tests__/host-browser-routes.test.ts +198 -0
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
- package/src/__tests__/host-cu-proxy.test.ts +171 -1
- package/src/__tests__/host-file-proxy.test.ts +185 -1
- package/src/__tests__/host-file-read-tool.test.ts +52 -0
- package/src/__tests__/host-proxy-interface.test.ts +165 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -11
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- package/src/__tests__/mcp-client-auth.test.ts +40 -4
- package/src/__tests__/mcp-health-check.test.ts +10 -3
- package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
- package/src/__tests__/migration-export-http.test.ts +61 -2
- package/src/__tests__/migration-export-streaming.test.ts +66 -0
- package/src/__tests__/migration-import-commit-http.test.ts +101 -1
- package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
- package/src/__tests__/oauth-apps-routes.test.ts +17 -12
- package/src/__tests__/oauth-cli.test.ts +707 -60
- package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
- package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
- package/src/__tests__/oauth-providers-routes.test.ts +50 -14
- package/src/__tests__/oauth-store.test.ts +1386 -182
- package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
- package/src/__tests__/onboarding-template-contract.test.ts +75 -57
- package/src/__tests__/openai-provider.test.ts +2 -2
- package/src/__tests__/outlook-categories.test.ts +1 -1
- package/src/__tests__/outlook-client-automation.test.ts +1 -1
- package/src/__tests__/outlook-compose-tools.test.ts +1 -1
- package/src/__tests__/outlook-email-watcher.test.ts +1 -1
- package/src/__tests__/outlook-follow-up.test.ts +1 -1
- package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
- package/src/__tests__/outlook-trash.test.ts +1 -1
- package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
- package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
- package/src/__tests__/permission-mode.test.ts +28 -56
- package/src/__tests__/platform-callback-registration.test.ts +19 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
- package/src/__tests__/proxy-approval-callback.test.ts +18 -0
- package/src/__tests__/require-fresh-approval.test.ts +40 -1
- package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
- package/src/__tests__/schedule-routes.test.ts +162 -0
- package/src/__tests__/secret-detection-handler.test.ts +84 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/set-permission-mode.test.ts +13 -250
- package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
- package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
- package/src/__tests__/slack-channel-config.test.ts +12 -15
- package/src/__tests__/subagent-detail.test.ts +44 -2
- package/src/__tests__/subagent-disposal.test.ts +1 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
- package/src/__tests__/subagent-manager-notify.test.ts +1 -0
- package/src/__tests__/subagent-notify-parent.test.ts +1 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/subagent-types.test.ts +1 -0
- package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
- package/src/__tests__/system-prompt.test.ts +72 -1
- package/src/__tests__/task-scheduler.test.ts +32 -6
- package/src/__tests__/telegram-config.test.ts +10 -13
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
- package/src/__tests__/top-level-renderer.test.ts +73 -1
- package/src/__tests__/transport-hints-queue.test.ts +14 -29
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- package/src/__tests__/v2-consent-policy.test.ts +103 -0
- package/src/acp/client-handler.ts +30 -4
- package/src/agent/loop.ts +12 -6
- package/src/approvals/guardian-request-resolvers.ts +21 -15
- package/src/browser-session/__tests__/manager.test.ts +297 -0
- package/src/browser-session/backends/cdp-inspect.ts +30 -0
- package/src/browser-session/backends/extension.ts +26 -0
- package/src/browser-session/backends/local.ts +24 -0
- package/src/browser-session/events.ts +164 -0
- package/src/browser-session/index.ts +27 -0
- package/src/browser-session/manager.ts +159 -0
- package/src/browser-session/types.ts +28 -0
- package/src/channels/__tests__/types.test.ts +134 -0
- package/src/channels/types.ts +53 -3
- package/src/cli/commands/browser-relay.ts +339 -409
- package/src/cli/commands/credentials.ts +3 -3
- package/src/cli/commands/email.ts +18 -13
- package/src/cli/commands/mcp.ts +16 -4
- package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
- package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
- package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
- package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
- package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
- package/src/cli/commands/oauth/apps.ts +7 -4
- package/src/cli/commands/oauth/connect.ts +6 -3
- package/src/cli/commands/oauth/disconnect.ts +1 -1
- package/src/cli/commands/oauth/providers.ts +200 -36
- package/src/cli/commands/oauth/shared.ts +5 -5
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
- package/src/cli/commands/platform/index.ts +107 -10
- package/src/cli/commands/usage.ts +10 -9
- package/src/cli/lib/daemon-credential-client.ts +4 -0
- package/src/cli/program.ts +1 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
- package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
- package/src/config/bundled-skills/contacts/SKILL.md +3 -0
- package/src/config/bundled-skills/document/SKILL.md +4 -0
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/outlook/SKILL.md +7 -0
- package/src/config/bundled-skills/subagent/SKILL.md +21 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
- package/src/config/bundled-skills/tasks/SKILL.md +5 -0
- package/src/config/env-registry.ts +14 -0
- package/src/config/env.ts +21 -0
- package/src/config/feature-flag-registry.json +44 -5
- package/src/config/loader.ts +56 -1
- package/src/config/sanitize-for-transfer.ts +47 -0
- package/src/config/schema.ts +46 -5
- package/src/config/schemas/host-browser.ts +66 -0
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/schemas/memory-retrieval.ts +103 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +8 -0
- package/src/config/types.ts +0 -1
- package/src/context/post-turn-tool-result-truncation.ts +176 -0
- package/src/context/window-manager.ts +19 -1
- package/src/credential-execution/approval-bridge.ts +49 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/context-overflow-approval.ts +5 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
- package/src/daemon/conversation-agent-loop.ts +58 -24
- package/src/daemon/conversation-attachments.ts +40 -0
- package/src/daemon/conversation-process.ts +48 -1
- package/src/daemon/conversation-runtime-assembly.ts +118 -36
- package/src/daemon/conversation-surfaces.ts +37 -36
- package/src/daemon/conversation-tool-setup.ts +74 -8
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +226 -8
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/conversations.ts +9 -140
- package/src/daemon/handlers/shared.ts +58 -0
- package/src/daemon/handlers/skills.ts +232 -37
- package/src/daemon/host-bash-proxy.ts +48 -13
- package/src/daemon/host-browser-proxy.ts +191 -0
- package/src/daemon/host-cu-proxy.ts +36 -11
- package/src/daemon/host-file-proxy.ts +57 -9
- package/src/daemon/lifecycle.ts +65 -11
- package/src/daemon/message-protocol.ts +7 -0
- package/src/daemon/message-types/conversations.ts +55 -13
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/messages.ts +5 -5
- package/src/daemon/message-types/skills.ts +10 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/server.ts +92 -12
- package/src/daemon/tool-side-effects.ts +6 -0
- package/src/daemon/transport-hints.ts +5 -24
- package/src/inbound/platform-callback-registration.ts +18 -17
- package/src/mcp/client.ts +59 -24
- package/src/memory/app-store.ts +31 -1
- package/src/memory/conversation-crud.ts +23 -0
- package/src/memory/conversation-starters-cadence.ts +76 -0
- package/src/memory/conversation-title-service.ts +5 -2
- package/src/memory/db-init.ts +12 -0
- package/src/memory/embedding-backend.test.ts +75 -0
- package/src/memory/embedding-backend.ts +131 -5
- package/src/memory/embedding-gemini.test.ts +54 -0
- package/src/memory/embedding-gemini.ts +20 -9
- package/src/memory/embedding-local.ts +176 -17
- package/src/memory/graph/consolidation.ts +10 -23
- package/src/memory/graph/extraction-job.ts +15 -0
- package/src/memory/graph/retriever.ts +40 -22
- package/src/memory/graph/store.test.ts +7 -3
- package/src/memory/graph/store.ts +47 -12
- package/src/memory/llm-usage-store.ts +45 -4
- package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
- package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
- package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
- package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
- package/src/memory/migrations/217-conversation-host-access.ts +40 -0
- package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +1 -0
- package/src/memory/schema/oauth.ts +18 -13
- package/src/oauth/AGENTS.md +76 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
- package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
- package/src/oauth/byo-connection.test.ts +8 -8
- package/src/oauth/byo-connection.ts +7 -7
- package/src/oauth/connect-orchestrator.ts +23 -21
- package/src/oauth/connect-types.ts +3 -3
- package/src/oauth/connection-resolver.test.ts +17 -4
- package/src/oauth/connection-resolver.ts +16 -16
- package/src/oauth/connection.ts +1 -1
- package/src/oauth/manual-token-connection.ts +13 -13
- package/src/oauth/oauth-store.ts +214 -100
- package/src/oauth/platform-connection.test.ts +3 -3
- package/src/oauth/platform-connection.ts +4 -4
- package/src/oauth/provider-serializer.ts +31 -5
- package/src/oauth/revoke.ts +76 -0
- package/src/oauth/seed-providers.ts +126 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -1
- package/src/permissions/v2-consent-policy.ts +87 -0
- package/src/prompts/system-prompt.ts +18 -21
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -105
- package/src/providers/anthropic/client.ts +1 -0
- package/src/providers/types.ts +1 -1
- package/src/runtime/AGENTS.md +23 -0
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
- package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
- package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
- package/src/runtime/assistant-event-hub.ts +2 -2
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
- package/src/runtime/auth/middleware.ts +98 -0
- package/src/runtime/auth/route-policy.ts +6 -7
- package/src/runtime/capability-tokens.ts +414 -0
- package/src/runtime/channel-approvals.ts +18 -5
- package/src/runtime/chrome-extension-registry.ts +332 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
- package/src/runtime/guardian-decision-types.ts +7 -0
- package/src/runtime/http-server.ts +425 -70
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
- package/src/runtime/migrations/migration-transport.ts +6 -0
- package/src/runtime/migrations/migration-wizard.ts +22 -2
- package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
- package/src/runtime/migrations/vbundle-builder.ts +145 -38
- package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
- package/src/runtime/migrations/vbundle-importer.ts +55 -5
- package/src/runtime/pending-interactions.ts +29 -13
- package/src/runtime/routes/approval-routes.ts +90 -16
- package/src/runtime/routes/browser-cdp-routes.ts +229 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
- package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
- package/src/runtime/routes/conversation-management-routes.ts +108 -0
- package/src/runtime/routes/conversation-routes.ts +301 -27
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/guardian-action-routes.ts +24 -13
- package/src/runtime/routes/host-browser-routes.ts +279 -0
- package/src/runtime/routes/host-file-routes.ts +9 -1
- package/src/runtime/routes/identity-routes.ts +259 -16
- package/src/runtime/routes/log-export-routes.ts +42 -22
- package/src/runtime/routes/memory-item-routes.ts +1 -7
- package/src/runtime/routes/migration-routes.ts +87 -2
- package/src/runtime/routes/oauth-apps.ts +15 -17
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/schedule-routes.ts +24 -11
- package/src/runtime/routes/settings-routes.ts +9 -97
- package/src/runtime/routes/skills-routes.ts +52 -2
- package/src/runtime/routes/subagents-routes.ts +14 -10
- package/src/runtime/routes/usage-routes.ts +8 -7
- package/src/runtime/routes/workspace-routes.test.ts +22 -0
- package/src/runtime/routes/workspace-routes.ts +8 -1
- package/src/runtime/routes/workspace-utils.ts +2 -0
- package/src/schedule/scheduler.ts +7 -5
- package/src/security/ces-credential-client.ts +20 -0
- package/src/security/ces-rpc-credential-backend.ts +17 -0
- package/src/security/credential-backend.ts +5 -0
- package/src/security/oauth2.ts +42 -25
- package/src/security/secure-keys.ts +118 -25
- package/src/security/token-manager.ts +23 -10
- package/src/skills/catalog-files.ts +492 -0
- package/src/subagent/manager.ts +131 -26
- package/src/subagent/types.ts +19 -0
- package/src/tools/apps/executors.ts +11 -2
- package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
- package/src/tools/browser/auth-detector.ts +43 -12
- package/src/tools/browser/browser-execution.ts +645 -340
- package/src/tools/browser/browser-manager.ts +36 -12
- package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
- package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
- package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
- package/src/tools/browser/cdp-client/errors.ts +34 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
- package/src/tools/browser/cdp-client/factory.ts +204 -0
- package/src/tools/browser/cdp-client/index.ts +14 -0
- package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
- package/src/tools/browser/cdp-client/types.ts +52 -0
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +1 -1
- package/src/tools/host-filesystem/read.ts +12 -15
- package/src/tools/host-filesystem/write.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +21 -16
- package/src/tools/permission-checker.ts +77 -82
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -0
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- package/src/tools/subagent/spawn.ts +47 -3
- package/src/tools/subagent/status.ts +2 -0
- package/src/tools/system/register.ts +2 -16
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/tools/terminal/shell.ts +21 -16
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/types.ts +2 -0
- package/src/util/platform.ts +14 -19
- package/src/workspace/top-level-renderer.ts +19 -1
- package/src/__tests__/chrome-cdp.test.ts +0 -419
- package/src/__tests__/permission-mode-sse.test.ts +0 -418
- package/src/__tests__/permission-mode-store.test.ts +0 -277
- package/src/browser-extension-relay/protocol.ts +0 -63
- package/src/browser-extension-relay/server.ts +0 -203
- package/src/config/schemas/sandbox.ts +0 -14
- package/src/permissions/permission-mode-store.ts +0 -180
- package/src/tools/browser/chrome-cdp.ts +0 -239
- package/src/tools/system/set-permission-mode.ts +0 -103
package/src/mcp/client.ts
CHANGED
|
@@ -35,6 +35,12 @@ export class McpClient {
|
|
|
35
35
|
| null = null;
|
|
36
36
|
private connected = false;
|
|
37
37
|
private oauthProvider: McpOAuthProvider | null = null;
|
|
38
|
+
private _lastError: Error | null = null;
|
|
39
|
+
|
|
40
|
+
/** The last connection error, if any. Null when connected or not yet attempted. */
|
|
41
|
+
get lastError(): Error | null {
|
|
42
|
+
return this._lastError;
|
|
43
|
+
}
|
|
38
44
|
|
|
39
45
|
get isConnected(): boolean {
|
|
40
46
|
return this.connected;
|
|
@@ -46,6 +52,16 @@ export class McpClient {
|
|
|
46
52
|
name: "vellum-assistant",
|
|
47
53
|
version: "1.0.0",
|
|
48
54
|
});
|
|
55
|
+
|
|
56
|
+
// Prevent SDK-internal transport errors (e.g. SSE reconnection auth
|
|
57
|
+
// failures) from surfacing as unhandled rejections that crash the daemon
|
|
58
|
+
// via the global unhandledRejection → shutdown handler.
|
|
59
|
+
this.client.onerror = (error) => {
|
|
60
|
+
log.warn(
|
|
61
|
+
{ serverId: this.serverId, err: error },
|
|
62
|
+
"MCP SDK transport error (non-fatal)",
|
|
63
|
+
);
|
|
64
|
+
};
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
async connect(transportConfig: McpTransport): Promise<void> {
|
|
@@ -102,34 +118,24 @@ export class McpClient {
|
|
|
102
118
|
}
|
|
103
119
|
this.transport = null;
|
|
104
120
|
|
|
105
|
-
if (isHttpTransport) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
(err instanceof Error &&
|
|
109
|
-
/\b(401|403|unauthorized|forbidden)\b/i.test(err.message)) ||
|
|
110
|
-
(err != null &&
|
|
111
|
-
typeof err === "object" &&
|
|
112
|
-
"code" in err &&
|
|
113
|
-
(err.code === 401 || err.code === 403));
|
|
114
|
-
|
|
115
|
-
if (isAuthError) {
|
|
116
|
-
// Auth-related — user can run `assistant mcp auth <name>` to authenticate.
|
|
117
|
-
log.info(
|
|
118
|
-
{ serverId: this.serverId, err },
|
|
119
|
-
"MCP server requires authentication",
|
|
120
|
-
);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Non-auth error (DNS, TLS, timeout, etc.) — log and re-throw
|
|
125
|
-
log.error(
|
|
121
|
+
if (isHttpTransport && isAuthRelatedError(err)) {
|
|
122
|
+
// Auth-related — user can run `assistant mcp auth <name>` to authenticate.
|
|
123
|
+
log.info(
|
|
126
124
|
{ serverId: this.serverId, err },
|
|
127
|
-
"MCP server
|
|
125
|
+
"MCP server requires authentication",
|
|
128
126
|
);
|
|
129
|
-
|
|
127
|
+
return;
|
|
130
128
|
}
|
|
131
129
|
|
|
132
|
-
|
|
130
|
+
// Non-auth error (DNS, TLS, timeout, etc.) — log but never propagate
|
|
131
|
+
// an MCP connection failure to the caller. The daemon must keep
|
|
132
|
+
// running even when individual MCP servers are unreachable.
|
|
133
|
+
this._lastError = err instanceof Error ? err : new Error(String(err));
|
|
134
|
+
log.error(
|
|
135
|
+
{ serverId: this.serverId, err },
|
|
136
|
+
"MCP server connection failed",
|
|
137
|
+
);
|
|
138
|
+
return;
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
this.connected = true;
|
|
@@ -258,3 +264,32 @@ export class McpClient {
|
|
|
258
264
|
}
|
|
259
265
|
}
|
|
260
266
|
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Returns true when `err` looks like an authentication / authorization failure
|
|
270
|
+
* from the MCP SDK or the remote server. Used to distinguish "needs auth"
|
|
271
|
+
* from genuine transport failures so we can log guidance instead of crashing.
|
|
272
|
+
*/
|
|
273
|
+
function isAuthRelatedError(err: unknown): boolean {
|
|
274
|
+
if (err instanceof UnauthorizedError) return true;
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
err instanceof Error &&
|
|
278
|
+
/\b(401|403|unauthorized|forbidden|authorizationCode is required|prepareTokenRequest)\b/i.test(
|
|
279
|
+
err.message,
|
|
280
|
+
)
|
|
281
|
+
) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (
|
|
286
|
+
err != null &&
|
|
287
|
+
typeof err === "object" &&
|
|
288
|
+
"code" in err &&
|
|
289
|
+
(err.code === 401 || err.code === 403)
|
|
290
|
+
) {
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return false;
|
|
295
|
+
}
|
package/src/memory/app-store.ts
CHANGED
|
@@ -64,6 +64,23 @@ export function isMultifileApp(app: AppDefinition): boolean {
|
|
|
64
64
|
return app.formatVersion === 2;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the effective HTML for an app. For single-file apps this is
|
|
69
|
+
* `htmlDefinition` (the root index.html). For multifile apps it reads the
|
|
70
|
+
* compiled `dist/index.html` and inlines JS/CSS assets so the result is a
|
|
71
|
+
* self-contained HTML string suitable for `loadHTMLString`.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveEffectiveAppHtml(app: AppDefinition): string {
|
|
74
|
+
if (!isMultifileApp(app)) return app.htmlDefinition;
|
|
75
|
+
|
|
76
|
+
const appDir = getAppDirPath(app.id);
|
|
77
|
+
const distIndex = join(appDir, "dist", "index.html");
|
|
78
|
+
if (existsSync(distIndex)) {
|
|
79
|
+
return inlineDistAssets(appDir, readFileSync(distIndex, "utf-8"));
|
|
80
|
+
}
|
|
81
|
+
return app.htmlDefinition;
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
/**
|
|
68
85
|
* Inline dist assets (main.js, main.css) into the compiled HTML so it can be
|
|
69
86
|
* delivered as a self-contained string via loadHTMLString/SSE without needing
|
|
@@ -75,7 +92,10 @@ export function inlineDistAssets(appDir: string, html: string): string {
|
|
|
75
92
|
// Inline main.js
|
|
76
93
|
const jsPath = join(distDir, "main.js");
|
|
77
94
|
if (existsSync(jsPath)) {
|
|
78
|
-
const js = readFileSync(jsPath, "utf-8").replace(
|
|
95
|
+
const js = readFileSync(jsPath, "utf-8").replace(
|
|
96
|
+
/<\/script>/g,
|
|
97
|
+
"<\\/script>",
|
|
98
|
+
);
|
|
79
99
|
html = html.replace(
|
|
80
100
|
/<script\s+type="module"\s+src="main\.js"\s*><\/script>/,
|
|
81
101
|
() => `<script type="module">${js}</script>`,
|
|
@@ -818,6 +838,16 @@ export function listAppFiles(appId: string): string[] {
|
|
|
818
838
|
return results.sort();
|
|
819
839
|
}
|
|
820
840
|
|
|
841
|
+
/**
|
|
842
|
+
* Check whether a file exists in the app directory.
|
|
843
|
+
* Path is validated to prevent traversal.
|
|
844
|
+
*/
|
|
845
|
+
export function appFileExists(appId: string, path: string): boolean {
|
|
846
|
+
validateId(appId);
|
|
847
|
+
const resolved = validateFilePath(appId, path);
|
|
848
|
+
return existsSync(resolved);
|
|
849
|
+
}
|
|
850
|
+
|
|
821
851
|
/**
|
|
822
852
|
* Read a file from the app directory.
|
|
823
853
|
* Path is validated to prevent traversal.
|
|
@@ -170,6 +170,7 @@ export interface ConversationRow {
|
|
|
170
170
|
originInterface: string | null;
|
|
171
171
|
forkParentConversationId: string | null;
|
|
172
172
|
forkParentMessageId: string | null;
|
|
173
|
+
hostAccess: number;
|
|
173
174
|
isAutoTitle: number;
|
|
174
175
|
scheduleJobId: string | null;
|
|
175
176
|
lastMessageAt: number | null;
|
|
@@ -196,6 +197,7 @@ export const parseConversation = createRowMapper<
|
|
|
196
197
|
originInterface: "originInterface",
|
|
197
198
|
forkParentConversationId: "forkParentConversationId",
|
|
198
199
|
forkParentMessageId: "forkParentMessageId",
|
|
200
|
+
hostAccess: "hostAccess",
|
|
199
201
|
isAutoTitle: "isAutoTitle",
|
|
200
202
|
scheduleJobId: "scheduleJobId",
|
|
201
203
|
lastMessageAt: "lastMessageAt",
|
|
@@ -245,6 +247,7 @@ export function createConversation(
|
|
|
245
247
|
source?: string;
|
|
246
248
|
scheduleJobId?: string;
|
|
247
249
|
groupId?: string;
|
|
250
|
+
hostAccess?: boolean;
|
|
248
251
|
},
|
|
249
252
|
) {
|
|
250
253
|
const db = getDb();
|
|
@@ -276,6 +279,7 @@ export function createConversation(
|
|
|
276
279
|
contextSummary: null as string | null,
|
|
277
280
|
contextCompactedMessageCount: 0,
|
|
278
281
|
contextCompactedAt: null as number | null,
|
|
282
|
+
hostAccess: opts.hostAccess ? 1 : 0,
|
|
279
283
|
conversationType,
|
|
280
284
|
source,
|
|
281
285
|
memoryScopeId,
|
|
@@ -388,6 +392,11 @@ export function getConversationMemoryScopeId(conversationId: string): string {
|
|
|
388
392
|
return conv?.memoryScopeId ?? "default";
|
|
389
393
|
}
|
|
390
394
|
|
|
395
|
+
export function getConversationHostAccess(conversationId: string): boolean {
|
|
396
|
+
const conv = getConversation(conversationId);
|
|
397
|
+
return conv?.hostAccess === 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
391
400
|
/**
|
|
392
401
|
* Fetch group_id for a conversation via raw SQL. group_id is NOT in the
|
|
393
402
|
* Drizzle schema (raw-query-only pattern), so ConversationRow doesn't
|
|
@@ -1123,6 +1132,20 @@ export function updateConversationContextWindow(
|
|
|
1123
1132
|
.run();
|
|
1124
1133
|
}
|
|
1125
1134
|
|
|
1135
|
+
export function updateConversationHostAccess(
|
|
1136
|
+
id: string,
|
|
1137
|
+
hostAccess: boolean,
|
|
1138
|
+
): void {
|
|
1139
|
+
const db = getDb();
|
|
1140
|
+
db.update(conversations)
|
|
1141
|
+
.set({
|
|
1142
|
+
hostAccess: hostAccess ? 1 : 0,
|
|
1143
|
+
updatedAt: Date.now(),
|
|
1144
|
+
})
|
|
1145
|
+
.where(eq(conversations.id, id))
|
|
1146
|
+
.run();
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1126
1149
|
/**
|
|
1127
1150
|
* Delete all conversations, messages, and related data (tool invocations,
|
|
1128
1151
|
* memory segments, etc.) from the daemon database.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cadence logic for conversation starters generation.
|
|
3
|
+
*
|
|
4
|
+
* Decides whether a new generation job should be enqueued based on how many
|
|
5
|
+
* active memory items have accumulated since the last generation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
9
|
+
|
|
10
|
+
import { getLogger } from "../util/logger.js";
|
|
11
|
+
import { getDb } from "./db.js";
|
|
12
|
+
import { enqueueMemoryJob } from "./jobs-store.js";
|
|
13
|
+
import { rawGet } from "./raw-query.js";
|
|
14
|
+
import { memoryCheckpoints, memoryJobs } from "./schema.js";
|
|
15
|
+
|
|
16
|
+
const log = getLogger("conversation-starters-cadence");
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check whether enough new memory items have accumulated to justify
|
|
20
|
+
* generating a fresh batch of conversation starters.
|
|
21
|
+
*/
|
|
22
|
+
export function maybeEnqueueConversationStartersJob(scopeId: string): void {
|
|
23
|
+
const db = getDb();
|
|
24
|
+
|
|
25
|
+
// Count total active memory items
|
|
26
|
+
const countRow = rawGet<{ c: number }>(
|
|
27
|
+
`SELECT COUNT(*) AS c FROM memory_graph_nodes WHERE fidelity != 'gone' AND scope_id = ?`,
|
|
28
|
+
scopeId,
|
|
29
|
+
);
|
|
30
|
+
const totalActive = countRow?.c ?? 0;
|
|
31
|
+
if (totalActive === 0) return;
|
|
32
|
+
|
|
33
|
+
// Read checkpoint: item count at last generation (scoped so each scope tracks independently)
|
|
34
|
+
const checkpointKey = `conversation_starters:item_count_at_last_gen:${scopeId}`;
|
|
35
|
+
const checkpoint = db
|
|
36
|
+
.select({ value: memoryCheckpoints.value })
|
|
37
|
+
.from(memoryCheckpoints)
|
|
38
|
+
.where(eq(memoryCheckpoints.key, checkpointKey))
|
|
39
|
+
.get();
|
|
40
|
+
const parsedLastCount = checkpoint ? parseInt(checkpoint.value, 10) : 0;
|
|
41
|
+
const lastCount = Number.isFinite(parsedLastCount) ? parsedLastCount : 0;
|
|
42
|
+
|
|
43
|
+
// Cadence formula
|
|
44
|
+
let threshold: number;
|
|
45
|
+
if (totalActive <= 10) {
|
|
46
|
+
threshold = 1;
|
|
47
|
+
} else if (totalActive <= 50) {
|
|
48
|
+
threshold = 5;
|
|
49
|
+
} else {
|
|
50
|
+
threshold = 10;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const checkpointAhead = totalActive < lastCount;
|
|
54
|
+
const delta = Math.max(0, totalActive - lastCount);
|
|
55
|
+
if (!checkpointAhead && delta < threshold) return;
|
|
56
|
+
|
|
57
|
+
// Dedup: don't enqueue if a pending/running job for this scope already exists
|
|
58
|
+
const existing = db
|
|
59
|
+
.select({ id: memoryJobs.id })
|
|
60
|
+
.from(memoryJobs)
|
|
61
|
+
.where(
|
|
62
|
+
and(
|
|
63
|
+
eq(memoryJobs.type, "generate_conversation_starters"),
|
|
64
|
+
inArray(memoryJobs.status, ["pending", "running"]),
|
|
65
|
+
sql`json_extract(${memoryJobs.payload}, '$.scopeId') = ${scopeId}`,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
.get();
|
|
69
|
+
if (existing) return;
|
|
70
|
+
|
|
71
|
+
enqueueMemoryJob("generate_conversation_starters", { scopeId });
|
|
72
|
+
log.info(
|
|
73
|
+
{ totalActive, lastCount, delta, threshold, scopeId, checkpointAhead },
|
|
74
|
+
"Enqueued conversation starters generation job",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -284,10 +284,13 @@ export function queueRegenerateConversationTitle(
|
|
|
284
284
|
*/
|
|
285
285
|
function buildTitleSystemPrompt(): string {
|
|
286
286
|
return [
|
|
287
|
-
"You generate
|
|
287
|
+
"You generate ultra-concise conversation titles. Output ONLY the title text — no explanation, no quotes, no markdown, no preamble.",
|
|
288
288
|
"",
|
|
289
289
|
"Rules:",
|
|
290
|
-
"-
|
|
290
|
+
"- 2–6 words. Titles longer than 6 words are unacceptable — ruthlessly compress",
|
|
291
|
+
"- Summarize the TOPIC, not the request or instructions",
|
|
292
|
+
"- Noun phrases are ideal (e.g. 'Auth Middleware Rewrite', 'Docker Volume Mounts')",
|
|
293
|
+
"- Do NOT echo back what the user asked you to do",
|
|
291
294
|
"- Do NOT respond to the conversation content",
|
|
292
295
|
"- Do NOT assess feasibility or comment on capabilities",
|
|
293
296
|
].join("\n");
|
package/src/memory/db-init.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
migrateContactsRolePrincipal,
|
|
59
59
|
migrateContactsUserFileColumn,
|
|
60
60
|
migrateConversationForkLineage,
|
|
61
|
+
migrateConversationHostAccess,
|
|
61
62
|
migrateConversationsLastMessageAt,
|
|
62
63
|
migrateConversationsThreadTypeIndex,
|
|
63
64
|
migrateCreateConversationGraphMemoryState,
|
|
@@ -110,9 +111,14 @@ import {
|
|
|
110
111
|
migrateOAuthProvidersBehaviorColumns,
|
|
111
112
|
migrateOAuthProvidersDisplayMetadata,
|
|
112
113
|
migrateOAuthProvidersFeatureFlag,
|
|
114
|
+
migrateOAuthProvidersLogoUrl,
|
|
113
115
|
migrateOAuthProvidersManagedServiceConfigKey,
|
|
114
116
|
migrateOAuthProvidersPingConfig,
|
|
115
117
|
migrateOAuthProvidersPingUrl,
|
|
118
|
+
migrateOAuthProvidersRefreshUrl,
|
|
119
|
+
migrateOAuthProvidersRevoke,
|
|
120
|
+
migrateOAuthProvidersScopeSeparator,
|
|
121
|
+
migrateOAuthProvidersTokenAuthMethodDefault,
|
|
116
122
|
migrateReminderRoutingIntent,
|
|
117
123
|
migrateRemindersToSchedules,
|
|
118
124
|
migrateRenameConversationTypeColumn,
|
|
@@ -352,6 +358,12 @@ export function initializeDb(): void {
|
|
|
352
358
|
migrateScheduleReuseConversation,
|
|
353
359
|
migrateMemoryRecallLogsQueryContext,
|
|
354
360
|
migrateLlmRequestLogsCreatedAtIndex,
|
|
361
|
+
migrateOAuthProvidersScopeSeparator,
|
|
362
|
+
migrateOAuthProvidersRefreshUrl,
|
|
363
|
+
migrateOAuthProvidersRevoke,
|
|
364
|
+
migrateOAuthProvidersTokenAuthMethodDefault,
|
|
365
|
+
migrateConversationHostAccess,
|
|
366
|
+
migrateOAuthProvidersLogoUrl,
|
|
355
367
|
];
|
|
356
368
|
|
|
357
369
|
// Run each migration step, catching and logging individual failures so one
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { AssistantConfig } from "../config/types.js";
|
|
4
|
+
import {
|
|
5
|
+
clearEmbeddingBackendCache,
|
|
6
|
+
resetLocalEmbeddingFailureState,
|
|
7
|
+
selectEmbeddingBackend,
|
|
8
|
+
} from "./embedding-backend.js";
|
|
9
|
+
|
|
10
|
+
const LOCAL_CONFIG = {
|
|
11
|
+
memory: {
|
|
12
|
+
embeddings: {
|
|
13
|
+
provider: "local",
|
|
14
|
+
localModel: "BAAI/bge-small-en-v1.5",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
} as unknown as AssistantConfig;
|
|
18
|
+
|
|
19
|
+
describe("embedding backend cache invalidation", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
clearEmbeddingBackendCache();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("clearEmbeddingBackendCache disposes cached backends before clearing", async () => {
|
|
25
|
+
const firstSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
|
|
26
|
+
expect(firstSelection.backend).not.toBeNull();
|
|
27
|
+
|
|
28
|
+
const dispose = mock();
|
|
29
|
+
(firstSelection.backend as { dispose?: () => void }).dispose = dispose;
|
|
30
|
+
|
|
31
|
+
clearEmbeddingBackendCache();
|
|
32
|
+
|
|
33
|
+
expect(dispose).toHaveBeenCalledTimes(1);
|
|
34
|
+
|
|
35
|
+
const secondSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
|
|
36
|
+
expect(secondSelection.backend).not.toBe(firstSelection.backend);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("resetLocalEmbeddingFailureState preserves live cached backends", async () => {
|
|
40
|
+
const firstSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
|
|
41
|
+
expect(firstSelection.backend).not.toBeNull();
|
|
42
|
+
|
|
43
|
+
const dispose = mock();
|
|
44
|
+
(firstSelection.backend as { dispose?: () => void }).dispose = dispose;
|
|
45
|
+
|
|
46
|
+
resetLocalEmbeddingFailureState();
|
|
47
|
+
|
|
48
|
+
expect(dispose).not.toHaveBeenCalled();
|
|
49
|
+
|
|
50
|
+
const secondSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
|
|
51
|
+
expect(secondSelection.backend).toBe(firstSelection.backend);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("resetLocalEmbeddingFailureState clears poisoned local backend retry state", async () => {
|
|
55
|
+
const firstSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
|
|
56
|
+
expect(firstSelection.backend).not.toBeNull();
|
|
57
|
+
|
|
58
|
+
const poisonedInitPromise = Promise.reject(new Error("poisoned"));
|
|
59
|
+
poisonedInitPromise.catch(() => {});
|
|
60
|
+
|
|
61
|
+
const backend = firstSelection.backend as unknown as {
|
|
62
|
+
delegate: unknown;
|
|
63
|
+
initPromise: Promise<unknown> | null;
|
|
64
|
+
};
|
|
65
|
+
backend.delegate = null;
|
|
66
|
+
backend.initPromise = poisonedInitPromise;
|
|
67
|
+
|
|
68
|
+
resetLocalEmbeddingFailureState();
|
|
69
|
+
|
|
70
|
+
expect(backend.initPromise).toBeNull();
|
|
71
|
+
|
|
72
|
+
const secondSelection = await selectEmbeddingBackend(LOCAL_CONFIG);
|
|
73
|
+
expect(secondSelection.backend).toBe(firstSelection.backend);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import { isAssistantFeatureFlagEnabled } from "../config/assistant-feature-flags.js";
|
|
3
4
|
import { getOllamaBaseUrlEnv } from "../config/env.js";
|
|
4
5
|
import type { AssistantConfig } from "../config/types.js";
|
|
6
|
+
import { MANAGED_PROVIDER_META } from "../providers/managed-proxy/constants.js";
|
|
7
|
+
import { resolveManagedProxyContext } from "../providers/managed-proxy/context.js";
|
|
5
8
|
import { getProviderKeyAsync } from "../security/secure-keys.js";
|
|
6
9
|
import { getLogger } from "../util/logger.js";
|
|
7
10
|
import { GeminiEmbeddingBackend } from "./embedding-gemini.js";
|
|
@@ -67,6 +70,16 @@ class LazyLocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
dispose(): void {
|
|
74
|
+
this.delegate?.dispose?.();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
resetForRetry(): void {
|
|
78
|
+
if (!this.delegate) {
|
|
79
|
+
this.initPromise = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
private async getDelegate(): Promise<EmbeddingBackend> {
|
|
71
84
|
if (this.delegate) return this.delegate;
|
|
72
85
|
if (!this.initPromise) {
|
|
@@ -176,12 +189,32 @@ function putInVectorCache(
|
|
|
176
189
|
|
|
177
190
|
/** Clear cached embedding backends and the in-memory vector cache. */
|
|
178
191
|
export function clearEmbeddingBackendCache(): void {
|
|
192
|
+
for (const backend of new Set(backendCache.values())) {
|
|
193
|
+
try {
|
|
194
|
+
backend.dispose?.();
|
|
195
|
+
} catch (err) {
|
|
196
|
+
log.warn(
|
|
197
|
+
{ err, provider: backend.provider, model: backend.model },
|
|
198
|
+
"Failed to dispose embedding backend during cache clear",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
179
202
|
backendCache.clear();
|
|
180
203
|
vectorCache.clear();
|
|
181
204
|
vectorCacheBytes = 0;
|
|
182
205
|
localBackendBroken = false;
|
|
183
206
|
}
|
|
184
207
|
|
|
208
|
+
/** Reset the sticky local-backend failure flag without evicting live backends. */
|
|
209
|
+
export function resetLocalEmbeddingFailureState(): void {
|
|
210
|
+
localBackendBroken = false;
|
|
211
|
+
for (const backend of new Set(backendCache.values())) {
|
|
212
|
+
if (backend instanceof LazyLocalEmbeddingBackend) {
|
|
213
|
+
backend.resetForRetry();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
185
218
|
function cacheKey(provider: string, model: string, extras?: string[]): string {
|
|
186
219
|
if (extras && extras.length > 0) {
|
|
187
220
|
return `${provider}:${model}:${extras.join(":")}`;
|
|
@@ -243,6 +276,7 @@ export interface EmbeddingBackend {
|
|
|
243
276
|
inputs: EmbeddingInput[],
|
|
244
277
|
options?: EmbeddingRequestOptions,
|
|
245
278
|
): Promise<number[][]>;
|
|
279
|
+
dispose?(): void;
|
|
246
280
|
}
|
|
247
281
|
|
|
248
282
|
export interface EmbeddingBackendSelection {
|
|
@@ -280,6 +314,44 @@ export async function selectEmbeddingBackend(
|
|
|
280
314
|
};
|
|
281
315
|
}
|
|
282
316
|
|
|
317
|
+
// When the managed-gemini-embeddings-enabled flag is on AND managed proxy
|
|
318
|
+
// prerequisites are satisfied, insert managed-proxy Gemini at the front of
|
|
319
|
+
// the auto chain so platform assistants use Vellum-managed Gemini embeddings.
|
|
320
|
+
if (
|
|
321
|
+
(requested === "auto" || requested === "gemini") &&
|
|
322
|
+
isAssistantFeatureFlagEnabled("managed-gemini-embeddings-enabled", config)
|
|
323
|
+
) {
|
|
324
|
+
const proxyCtx = await resolveManagedProxyContext();
|
|
325
|
+
if (proxyCtx.enabled) {
|
|
326
|
+
const meta = MANAGED_PROVIDER_META["gemini"];
|
|
327
|
+
if (meta?.managed && meta.proxyPath) {
|
|
328
|
+
const managedBaseUrl = `${proxyCtx.platformBaseUrl}${meta.proxyPath}`;
|
|
329
|
+
const managedModel = config.memory.embeddings.geminiModel;
|
|
330
|
+
const managedDimensions =
|
|
331
|
+
config.memory.embeddings.geminiDimensions ?? 3072;
|
|
332
|
+
const extras = geminiCacheExtras(config);
|
|
333
|
+
return {
|
|
334
|
+
backend: getCachedOrCreate(
|
|
335
|
+
"gemini",
|
|
336
|
+
managedModel,
|
|
337
|
+
() =>
|
|
338
|
+
new GeminiEmbeddingBackend(
|
|
339
|
+
proxyCtx.assistantApiKey,
|
|
340
|
+
managedModel,
|
|
341
|
+
{
|
|
342
|
+
taskType: config.memory.embeddings.geminiTaskType,
|
|
343
|
+
dimensions: managedDimensions,
|
|
344
|
+
managedBaseUrl,
|
|
345
|
+
},
|
|
346
|
+
),
|
|
347
|
+
[...extras, "managed"],
|
|
348
|
+
),
|
|
349
|
+
reason: null,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
283
355
|
// Auto order: local → openai → gemini → ollama
|
|
284
356
|
const order: EmbeddingProviderName[] =
|
|
285
357
|
requested === "auto"
|
|
@@ -329,11 +401,18 @@ export async function selectEmbeddingBackend(
|
|
|
329
401
|
case "gemini": {
|
|
330
402
|
const geminiKey = await getProviderKeyAsync("gemini");
|
|
331
403
|
if (!geminiKey) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
404
|
+
// Check managed cache variant first so a warm managed backend
|
|
405
|
+
// survives transient proxy-context blips, then non-managed.
|
|
406
|
+
const cached =
|
|
407
|
+
getCached("gemini", config.memory.embeddings.geminiModel, [
|
|
408
|
+
...geminiCacheExtras(config),
|
|
409
|
+
"managed",
|
|
410
|
+
]) ??
|
|
411
|
+
getCached(
|
|
412
|
+
"gemini",
|
|
413
|
+
config.memory.embeddings.geminiModel,
|
|
414
|
+
geminiCacheExtras(config),
|
|
415
|
+
);
|
|
337
416
|
if (cached) return { backend: cached, reason: null };
|
|
338
417
|
continue;
|
|
339
418
|
}
|
|
@@ -614,6 +693,53 @@ async function selectFallbackBackends(
|
|
|
614
693
|
geminiCacheExtras(config),
|
|
615
694
|
),
|
|
616
695
|
);
|
|
696
|
+
} else if (
|
|
697
|
+
isAssistantFeatureFlagEnabled(
|
|
698
|
+
"managed-gemini-embeddings-enabled",
|
|
699
|
+
config,
|
|
700
|
+
)
|
|
701
|
+
) {
|
|
702
|
+
// Try managed proxy Gemini as fallback when no direct key exists.
|
|
703
|
+
const proxyCtx = await resolveManagedProxyContext();
|
|
704
|
+
const meta = MANAGED_PROVIDER_META["gemini"];
|
|
705
|
+
if (proxyCtx.enabled && meta?.managed && meta.proxyPath) {
|
|
706
|
+
const managedBaseUrl = `${proxyCtx.platformBaseUrl}${meta.proxyPath}`;
|
|
707
|
+
const managedModel = config.memory.embeddings.geminiModel;
|
|
708
|
+
const managedDimensions =
|
|
709
|
+
config.memory.embeddings.geminiDimensions ?? 3072;
|
|
710
|
+
const extras = geminiCacheExtras(config);
|
|
711
|
+
backends.push(
|
|
712
|
+
getCachedOrCreate(
|
|
713
|
+
"gemini",
|
|
714
|
+
managedModel,
|
|
715
|
+
() =>
|
|
716
|
+
new GeminiEmbeddingBackend(
|
|
717
|
+
proxyCtx.assistantApiKey,
|
|
718
|
+
managedModel,
|
|
719
|
+
{
|
|
720
|
+
taskType: config.memory.embeddings.geminiTaskType,
|
|
721
|
+
dimensions: managedDimensions,
|
|
722
|
+
managedBaseUrl,
|
|
723
|
+
},
|
|
724
|
+
),
|
|
725
|
+
[...extras, "managed"],
|
|
726
|
+
),
|
|
727
|
+
);
|
|
728
|
+
} else {
|
|
729
|
+
// Check managed cache variant first, then non-managed, so a warm
|
|
730
|
+
// managed backend survives transient proxy-context blips.
|
|
731
|
+
const cached =
|
|
732
|
+
getCached("gemini", config.memory.embeddings.geminiModel, [
|
|
733
|
+
...geminiCacheExtras(config),
|
|
734
|
+
"managed",
|
|
735
|
+
]) ??
|
|
736
|
+
getCached(
|
|
737
|
+
"gemini",
|
|
738
|
+
config.memory.embeddings.geminiModel,
|
|
739
|
+
geminiCacheExtras(config),
|
|
740
|
+
);
|
|
741
|
+
if (cached) backends.push(cached);
|
|
742
|
+
}
|
|
617
743
|
} else {
|
|
618
744
|
// Preserve cached backend on transient credential-store failures.
|
|
619
745
|
const cached = getCached(
|
|
@@ -253,4 +253,58 @@ describe("GeminiEmbeddingBackend", () => {
|
|
|
253
253
|
expect(result[1]).toEqual([0.2, 0.4]);
|
|
254
254
|
});
|
|
255
255
|
});
|
|
256
|
+
|
|
257
|
+
describe("managed proxy transport", () => {
|
|
258
|
+
test("routes through managed proxy base URL when managedBaseUrl is set", async () => {
|
|
259
|
+
const backend = new GeminiEmbeddingBackend(
|
|
260
|
+
"ast-managed-key",
|
|
261
|
+
"gemini-embedding-2-preview",
|
|
262
|
+
{
|
|
263
|
+
managedBaseUrl:
|
|
264
|
+
"https://platform.example.com/v1/runtime-proxy/gemini",
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
await backend.embed(["hello"]);
|
|
268
|
+
|
|
269
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
270
|
+
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
271
|
+
expect(url).toBe(
|
|
272
|
+
"https://platform.example.com/v1/runtime-proxy/gemini/v1beta/models/gemini-embedding-2-preview:embedContent",
|
|
273
|
+
);
|
|
274
|
+
// Should NOT have key= query param
|
|
275
|
+
expect(url).not.toContain("key=");
|
|
276
|
+
// Should have Bearer auth header
|
|
277
|
+
const headers = init.headers as Record<string, string>;
|
|
278
|
+
expect(headers["Authorization"]).toBe("Bearer ast-managed-key");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("uses direct Google API URL when managedBaseUrl is not set", async () => {
|
|
282
|
+
const backend = new GeminiEmbeddingBackend("direct-key", "test-model");
|
|
283
|
+
await backend.embed(["hello"]);
|
|
284
|
+
|
|
285
|
+
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
286
|
+
expect(url).toContain("generativelanguage.googleapis.com");
|
|
287
|
+
expect(url).toContain("key=direct-key");
|
|
288
|
+
// Should NOT have Authorization header
|
|
289
|
+
const headers = init.headers as Record<string, string>;
|
|
290
|
+
expect(headers["Authorization"]).toBeUndefined();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("includes outputDimensionality with managed proxy", async () => {
|
|
294
|
+
const backend = new GeminiEmbeddingBackend(
|
|
295
|
+
"ast-managed-key",
|
|
296
|
+
"gemini-embedding-2-preview",
|
|
297
|
+
{
|
|
298
|
+
managedBaseUrl:
|
|
299
|
+
"https://platform.example.com/v1/runtime-proxy/gemini",
|
|
300
|
+
dimensions: 3072,
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
await backend.embed(["hello"]);
|
|
304
|
+
|
|
305
|
+
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit];
|
|
306
|
+
const body = JSON.parse(init.body as string);
|
|
307
|
+
expect(body.outputDimensionality).toBe(3072);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
256
310
|
});
|