@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
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// Silence the logger from cdp-inspect-client.
|
|
4
|
+
mock.module("../../../../util/logger.js", () => ({
|
|
5
|
+
getLogger: () => ({
|
|
6
|
+
info: () => {},
|
|
7
|
+
debug: () => {},
|
|
8
|
+
warn: () => {},
|
|
9
|
+
error: () => {},
|
|
10
|
+
}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Import under test AFTER mock.module calls so that the module's
|
|
14
|
+
// top-level logger import resolves to our fake.
|
|
15
|
+
const { CdpInspectClient, createCdpInspectClient } =
|
|
16
|
+
await import("../cdp-inspect-client.js");
|
|
17
|
+
const { CdpError } = await import("../errors.js");
|
|
18
|
+
const { CdpWsTransportError } = await import("../cdp-inspect/ws-transport.js");
|
|
19
|
+
|
|
20
|
+
type CdpInspectClientInstance = InstanceType<typeof CdpInspectClient>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Minimal fake CdpWsTransport used by the test harness below. The
|
|
24
|
+
* handler is per-send so individual tests can model success, CDP
|
|
25
|
+
* errors, transport errors, and abort behavior on specific methods.
|
|
26
|
+
*/
|
|
27
|
+
interface FakeTransportOptions {
|
|
28
|
+
onSend?: (
|
|
29
|
+
method: string,
|
|
30
|
+
params: Record<string, unknown> | undefined,
|
|
31
|
+
opts: { sessionId?: string; signal?: AbortSignal },
|
|
32
|
+
) => unknown | Promise<unknown>;
|
|
33
|
+
trackSends?: Array<{
|
|
34
|
+
method: string;
|
|
35
|
+
params?: Record<string, unknown>;
|
|
36
|
+
sessionId?: string;
|
|
37
|
+
}>;
|
|
38
|
+
trackDisposeCount?: { count: number };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createFakeTransport(options: FakeTransportOptions) {
|
|
42
|
+
const transport = {
|
|
43
|
+
send: async <T = unknown>(
|
|
44
|
+
method: string,
|
|
45
|
+
params?: Record<string, unknown>,
|
|
46
|
+
opts?: { sessionId?: string; signal?: AbortSignal },
|
|
47
|
+
): Promise<T> => {
|
|
48
|
+
options.trackSends?.push({
|
|
49
|
+
method,
|
|
50
|
+
params,
|
|
51
|
+
sessionId: opts?.sessionId,
|
|
52
|
+
});
|
|
53
|
+
if (options.onSend) {
|
|
54
|
+
const result = await options.onSend(method, params, opts ?? {});
|
|
55
|
+
return result as T;
|
|
56
|
+
}
|
|
57
|
+
return undefined as T;
|
|
58
|
+
},
|
|
59
|
+
addEventListener: () => () => {},
|
|
60
|
+
dispose: () => {
|
|
61
|
+
if (options.trackDisposeCount) {
|
|
62
|
+
options.trackDisposeCount.count += 1;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
return transport;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build a client wired to mocked discovery + transport helpers. The
|
|
71
|
+
* caller supplies handlers for the moving pieces; everything else
|
|
72
|
+
* defaults to a happy-path attach.
|
|
73
|
+
*/
|
|
74
|
+
interface HarnessOptions {
|
|
75
|
+
probeImpl?: (opts: unknown) => Promise<{
|
|
76
|
+
browser: string;
|
|
77
|
+
protocolVersion: string;
|
|
78
|
+
webSocketDebuggerUrl: string;
|
|
79
|
+
}>;
|
|
80
|
+
listImpl?: (opts: unknown) => Promise<
|
|
81
|
+
Array<{
|
|
82
|
+
id: string;
|
|
83
|
+
type: string;
|
|
84
|
+
title: string;
|
|
85
|
+
url: string;
|
|
86
|
+
webSocketDebuggerUrl: string;
|
|
87
|
+
}>
|
|
88
|
+
>;
|
|
89
|
+
connectImpl?: (
|
|
90
|
+
url: string,
|
|
91
|
+
opts?: { connectTimeoutMs?: number },
|
|
92
|
+
) => Promise<ReturnType<typeof createFakeTransport>>;
|
|
93
|
+
transportOnSend?: FakeTransportOptions["onSend"];
|
|
94
|
+
conversationId?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
interface Harness {
|
|
98
|
+
client: CdpInspectClientInstance;
|
|
99
|
+
sends: Array<{
|
|
100
|
+
method: string;
|
|
101
|
+
params?: Record<string, unknown>;
|
|
102
|
+
sessionId?: string;
|
|
103
|
+
}>;
|
|
104
|
+
disposeCount: { count: number };
|
|
105
|
+
probeCalls: number;
|
|
106
|
+
listCalls: number;
|
|
107
|
+
connectCalls: number;
|
|
108
|
+
attachCallCount: () => number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function createHarness(opts: HarnessOptions = {}): Harness {
|
|
112
|
+
const sends: Array<{
|
|
113
|
+
method: string;
|
|
114
|
+
params?: Record<string, unknown>;
|
|
115
|
+
sessionId?: string;
|
|
116
|
+
}> = [];
|
|
117
|
+
const disposeCount = { count: 0 };
|
|
118
|
+
let probeCalls = 0;
|
|
119
|
+
let listCalls = 0;
|
|
120
|
+
let connectCalls = 0;
|
|
121
|
+
|
|
122
|
+
// Track Target.attachToTarget specifically so tests can assert
|
|
123
|
+
// how many attach attempts the client has made. The counter is
|
|
124
|
+
// bumped ONLY in the default happy-path branch so tests that
|
|
125
|
+
// install a custom `transportOnSend` (and therefore model their
|
|
126
|
+
// own attach semantics) can't accidentally double-count.
|
|
127
|
+
const attachSends: Array<unknown> = [];
|
|
128
|
+
|
|
129
|
+
const defaultOnSend: FakeTransportOptions["onSend"] = (method) => {
|
|
130
|
+
if (method === "Target.attachToTarget") {
|
|
131
|
+
attachSends.push(method);
|
|
132
|
+
return { sessionId: "fake-session-id" };
|
|
133
|
+
}
|
|
134
|
+
return { ok: true };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const transportOnSend: FakeTransportOptions["onSend"] = async (
|
|
138
|
+
method,
|
|
139
|
+
params,
|
|
140
|
+
o,
|
|
141
|
+
) => {
|
|
142
|
+
if (opts.transportOnSend) {
|
|
143
|
+
return opts.transportOnSend(method, params, o);
|
|
144
|
+
}
|
|
145
|
+
return defaultOnSend!(method, params, o);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const client = createCdpInspectClient(opts.conversationId ?? "conv-1", {
|
|
149
|
+
host: "127.0.0.1",
|
|
150
|
+
port: 9222,
|
|
151
|
+
discoveryTimeoutMs: 100,
|
|
152
|
+
wsConnectTimeoutMs: 100,
|
|
153
|
+
helpers: {
|
|
154
|
+
probeDevToolsJsonVersion: async (probeOpts: unknown) => {
|
|
155
|
+
probeCalls += 1;
|
|
156
|
+
if (opts.probeImpl) return opts.probeImpl(probeOpts);
|
|
157
|
+
return {
|
|
158
|
+
browser: "Chrome/125.0.0.0",
|
|
159
|
+
protocolVersion: "1.3",
|
|
160
|
+
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
listDevToolsTargets: async (listOpts: unknown) => {
|
|
164
|
+
listCalls += 1;
|
|
165
|
+
if (opts.listImpl) return opts.listImpl(listOpts);
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
id: "target-1",
|
|
169
|
+
type: "page",
|
|
170
|
+
title: "Example",
|
|
171
|
+
url: "https://example.com/",
|
|
172
|
+
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/target-1",
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
},
|
|
176
|
+
// pickDefaultTarget uses the real implementation — it's pure.
|
|
177
|
+
connectCdpWsTransport: async (
|
|
178
|
+
url: string,
|
|
179
|
+
connectOpts?: { connectTimeoutMs?: number },
|
|
180
|
+
) => {
|
|
181
|
+
connectCalls += 1;
|
|
182
|
+
if (opts.connectImpl) return opts.connectImpl(url, connectOpts);
|
|
183
|
+
return createFakeTransport({
|
|
184
|
+
onSend: transportOnSend,
|
|
185
|
+
trackSends: sends,
|
|
186
|
+
trackDisposeCount: disposeCount,
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
client,
|
|
194
|
+
sends,
|
|
195
|
+
disposeCount,
|
|
196
|
+
get probeCalls() {
|
|
197
|
+
return probeCalls;
|
|
198
|
+
},
|
|
199
|
+
get listCalls() {
|
|
200
|
+
return listCalls;
|
|
201
|
+
},
|
|
202
|
+
get connectCalls() {
|
|
203
|
+
return connectCalls;
|
|
204
|
+
},
|
|
205
|
+
attachCallCount: () => attachSends.length,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
describe("CdpInspectClient", () => {
|
|
210
|
+
beforeEach(() => {
|
|
211
|
+
// no-op — each test gets its own harness
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("kind is 'cdp-inspect' and exposes conversationId", () => {
|
|
215
|
+
const { client } = createHarness({ conversationId: "conv-kind" });
|
|
216
|
+
expect(client).toBeInstanceOf(CdpInspectClient);
|
|
217
|
+
expect(client.kind).toBe("cdp-inspect");
|
|
218
|
+
expect(client.conversationId).toBe("conv-kind");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("send() probes version, lists targets, attaches, and forwards the call", async () => {
|
|
222
|
+
const harness = createHarness({
|
|
223
|
+
transportOnSend: (method) => {
|
|
224
|
+
if (method === "Target.attachToTarget") {
|
|
225
|
+
return { sessionId: "session-abc" };
|
|
226
|
+
}
|
|
227
|
+
if (method === "Browser.getVersion") {
|
|
228
|
+
return { product: "HeadlessChrome/125.0.0.0" };
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
const result = await harness.client.send<{ product: string }>(
|
|
234
|
+
"Browser.getVersion",
|
|
235
|
+
);
|
|
236
|
+
expect(result).toEqual({ product: "HeadlessChrome/125.0.0.0" });
|
|
237
|
+
expect(harness.probeCalls).toBe(1);
|
|
238
|
+
expect(harness.listCalls).toBe(1);
|
|
239
|
+
expect(harness.connectCalls).toBe(1);
|
|
240
|
+
// One attach + one forwarded Browser.getVersion.
|
|
241
|
+
expect(harness.sends).toEqual([
|
|
242
|
+
{
|
|
243
|
+
method: "Target.attachToTarget",
|
|
244
|
+
params: { targetId: "target-1", flatten: true },
|
|
245
|
+
sessionId: undefined,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
method: "Browser.getVersion",
|
|
249
|
+
params: undefined,
|
|
250
|
+
sessionId: "session-abc",
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("multiple send() calls share a single attach", async () => {
|
|
256
|
+
const harness = createHarness();
|
|
257
|
+
await harness.client.send("Runtime.enable");
|
|
258
|
+
await harness.client.send("Page.enable");
|
|
259
|
+
await harness.client.send("DOM.enable");
|
|
260
|
+
expect(harness.probeCalls).toBe(1);
|
|
261
|
+
expect(harness.listCalls).toBe(1);
|
|
262
|
+
expect(harness.connectCalls).toBe(1);
|
|
263
|
+
expect(harness.attachCallCount()).toBe(1);
|
|
264
|
+
expect(harness.sends.length).toBe(4); // 1 attach + 3 forwarded
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("concurrent send() calls share a single in-flight attach", async () => {
|
|
268
|
+
const harness = createHarness();
|
|
269
|
+
await Promise.all([
|
|
270
|
+
harness.client.send("Runtime.enable"),
|
|
271
|
+
harness.client.send("Page.enable"),
|
|
272
|
+
harness.client.send("DOM.enable"),
|
|
273
|
+
]);
|
|
274
|
+
expect(harness.probeCalls).toBe(1);
|
|
275
|
+
expect(harness.listCalls).toBe(1);
|
|
276
|
+
expect(harness.connectCalls).toBe(1);
|
|
277
|
+
expect(harness.attachCallCount()).toBe(1);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("send() retries ensureSession after an initial attach failure", async () => {
|
|
281
|
+
// First probe call rejects (simulating e.g. Chrome not yet listening).
|
|
282
|
+
// Second probe call succeeds. Because the cached sessionPromise must
|
|
283
|
+
// be cleared on rejection, the second send() performs a full retry.
|
|
284
|
+
let probeCount = 0;
|
|
285
|
+
const harness = createHarness({
|
|
286
|
+
probeImpl: async () => {
|
|
287
|
+
probeCount += 1;
|
|
288
|
+
if (probeCount === 1) {
|
|
289
|
+
throw new Error("connect ECONNREFUSED");
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
browser: "Chrome/125.0.0.0",
|
|
293
|
+
protocolVersion: "1.3",
|
|
294
|
+
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
|
|
295
|
+
};
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
let firstErr: unknown;
|
|
300
|
+
try {
|
|
301
|
+
await harness.client.send("Browser.getVersion");
|
|
302
|
+
} catch (err) {
|
|
303
|
+
firstErr = err;
|
|
304
|
+
}
|
|
305
|
+
expect(firstErr).toBeInstanceOf(CdpError);
|
|
306
|
+
expect((firstErr as InstanceType<typeof CdpError>).code).toBe(
|
|
307
|
+
"transport_error",
|
|
308
|
+
);
|
|
309
|
+
expect(probeCount).toBe(1);
|
|
310
|
+
expect(harness.connectCalls).toBe(0);
|
|
311
|
+
|
|
312
|
+
// Second call — cached promise was cleared, so probe + list +
|
|
313
|
+
// connect + attach all run again, then the forwarded call
|
|
314
|
+
// resolves normally. listCalls is only 1 because the first
|
|
315
|
+
// attempt threw inside probeDevToolsJsonVersion before it ever
|
|
316
|
+
// reached listDevToolsTargets.
|
|
317
|
+
const result = await harness.client.send<{ ok: boolean }>(
|
|
318
|
+
"Browser.getVersion",
|
|
319
|
+
);
|
|
320
|
+
expect(result).toEqual({ ok: true });
|
|
321
|
+
expect(probeCount).toBe(2);
|
|
322
|
+
expect(harness.listCalls).toBe(1);
|
|
323
|
+
expect(harness.connectCalls).toBe(1);
|
|
324
|
+
expect(harness.attachCallCount()).toBe(1);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("send() maps CDP protocol errors from attach to CdpError 'cdp_error'", async () => {
|
|
328
|
+
const harness = createHarness({
|
|
329
|
+
transportOnSend: async (method) => {
|
|
330
|
+
if (method === "Target.attachToTarget") {
|
|
331
|
+
throw new CdpWsTransportError(
|
|
332
|
+
"cdp_error",
|
|
333
|
+
"No target with given id found",
|
|
334
|
+
{
|
|
335
|
+
cdpMethod: "Target.attachToTarget",
|
|
336
|
+
cdpCode: -32602,
|
|
337
|
+
cdpMessage: "No target with given id found",
|
|
338
|
+
},
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return undefined;
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
let caught: unknown;
|
|
346
|
+
try {
|
|
347
|
+
await harness.client.send("Browser.getVersion");
|
|
348
|
+
} catch (err) {
|
|
349
|
+
caught = err;
|
|
350
|
+
}
|
|
351
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
352
|
+
const cdpErr = caught as InstanceType<typeof CdpError>;
|
|
353
|
+
expect(cdpErr.code).toBe("cdp_error");
|
|
354
|
+
expect(cdpErr.message).toBe("No target with given id found");
|
|
355
|
+
expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
|
|
356
|
+
expect(cdpErr.underlying).toBeInstanceOf(CdpWsTransportError);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("send() maps transport failures during attach to CdpError 'transport_error'", async () => {
|
|
360
|
+
const harness = createHarness({
|
|
361
|
+
connectImpl: async () => {
|
|
362
|
+
throw new CdpWsTransportError(
|
|
363
|
+
"transport_error",
|
|
364
|
+
"websocket closed before open",
|
|
365
|
+
);
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
let caught: unknown;
|
|
369
|
+
try {
|
|
370
|
+
await harness.client.send("Browser.getVersion");
|
|
371
|
+
} catch (err) {
|
|
372
|
+
caught = err;
|
|
373
|
+
}
|
|
374
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
375
|
+
const cdpErr = caught as InstanceType<typeof CdpError>;
|
|
376
|
+
expect(cdpErr.code).toBe("transport_error");
|
|
377
|
+
expect(cdpErr.message).toBe("websocket closed before open");
|
|
378
|
+
expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("send() with an already-aborted signal throws 'aborted' without touching the transport", async () => {
|
|
382
|
+
const harness = createHarness();
|
|
383
|
+
const controller = new AbortController();
|
|
384
|
+
controller.abort();
|
|
385
|
+
let caught: unknown;
|
|
386
|
+
try {
|
|
387
|
+
await harness.client.send(
|
|
388
|
+
"Browser.getVersion",
|
|
389
|
+
undefined,
|
|
390
|
+
controller.signal,
|
|
391
|
+
);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
caught = err;
|
|
394
|
+
}
|
|
395
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
396
|
+
const cdpErr = caught as InstanceType<typeof CdpError>;
|
|
397
|
+
expect(cdpErr.code).toBe("aborted");
|
|
398
|
+
expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
|
|
399
|
+
// Nothing ran — no discovery, no connect, no transport sends.
|
|
400
|
+
expect(harness.probeCalls).toBe(0);
|
|
401
|
+
expect(harness.listCalls).toBe(0);
|
|
402
|
+
expect(harness.connectCalls).toBe(0);
|
|
403
|
+
expect(harness.sends.length).toBe(0);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("send() classifies as 'aborted' when the signal fires during attach", async () => {
|
|
407
|
+
const controller = new AbortController();
|
|
408
|
+
const harness = createHarness({
|
|
409
|
+
probeImpl: async () => {
|
|
410
|
+
// Simulate caller aborting while discovery is in flight.
|
|
411
|
+
// Discovery itself throws a generic error (as real fetch
|
|
412
|
+
// would), and the abort flag is flipped — we expect the
|
|
413
|
+
// resulting CdpError to carry code "aborted".
|
|
414
|
+
controller.abort();
|
|
415
|
+
throw new Error("aborted during fetch");
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
let caught: unknown;
|
|
419
|
+
try {
|
|
420
|
+
await harness.client.send(
|
|
421
|
+
"Browser.getVersion",
|
|
422
|
+
undefined,
|
|
423
|
+
controller.signal,
|
|
424
|
+
);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
caught = err;
|
|
427
|
+
}
|
|
428
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
429
|
+
const cdpErr = caught as InstanceType<typeof CdpError>;
|
|
430
|
+
expect(cdpErr.code).toBe("aborted");
|
|
431
|
+
expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("send() classifies as 'aborted' when the signal fires during the forwarded call", async () => {
|
|
435
|
+
const controller = new AbortController();
|
|
436
|
+
const harness = createHarness({
|
|
437
|
+
transportOnSend: async (method) => {
|
|
438
|
+
if (method === "Target.attachToTarget") {
|
|
439
|
+
return { sessionId: "session-abc" };
|
|
440
|
+
}
|
|
441
|
+
// Simulate the transport throwing an abort error after
|
|
442
|
+
// the caller aborts mid-call.
|
|
443
|
+
controller.abort();
|
|
444
|
+
throw new CdpWsTransportError("aborted", "aborted during send", {
|
|
445
|
+
cdpMethod: method,
|
|
446
|
+
});
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
let caught: unknown;
|
|
450
|
+
try {
|
|
451
|
+
await harness.client.send(
|
|
452
|
+
"Page.navigate",
|
|
453
|
+
{ url: "about:blank" },
|
|
454
|
+
controller.signal,
|
|
455
|
+
);
|
|
456
|
+
} catch (err) {
|
|
457
|
+
caught = err;
|
|
458
|
+
}
|
|
459
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
460
|
+
const cdpErr = caught as InstanceType<typeof CdpError>;
|
|
461
|
+
expect(cdpErr.code).toBe("aborted");
|
|
462
|
+
expect(cdpErr.cdpMethod).toBe("Page.navigate");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("send() maps forwarded CDP protocol errors to 'cdp_error'", async () => {
|
|
466
|
+
const harness = createHarness({
|
|
467
|
+
transportOnSend: async (method) => {
|
|
468
|
+
if (method === "Target.attachToTarget") {
|
|
469
|
+
return { sessionId: "session-abc" };
|
|
470
|
+
}
|
|
471
|
+
throw new CdpWsTransportError("cdp_error", "invalid expression", {
|
|
472
|
+
cdpMethod: method,
|
|
473
|
+
cdpCode: -32000,
|
|
474
|
+
cdpMessage: "invalid expression",
|
|
475
|
+
});
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
let caught: unknown;
|
|
479
|
+
try {
|
|
480
|
+
await harness.client.send("Runtime.evaluate", { expression: "??" });
|
|
481
|
+
} catch (err) {
|
|
482
|
+
caught = err;
|
|
483
|
+
}
|
|
484
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
485
|
+
const cdpErr = caught as InstanceType<typeof CdpError>;
|
|
486
|
+
expect(cdpErr.code).toBe("cdp_error");
|
|
487
|
+
expect(cdpErr.message).toBe("invalid expression");
|
|
488
|
+
expect(cdpErr.cdpMethod).toBe("Runtime.evaluate");
|
|
489
|
+
expect(cdpErr.cdpParams).toEqual({ expression: "??" });
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("dispose() is idempotent and tears down the underlying transport", async () => {
|
|
493
|
+
const harness = createHarness();
|
|
494
|
+
await harness.client.send("Browser.getVersion");
|
|
495
|
+
harness.client.dispose();
|
|
496
|
+
// dispose schedules transport.dispose on the resolved attach
|
|
497
|
+
// promise's then() — flush microtasks.
|
|
498
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
499
|
+
expect(harness.disposeCount.count).toBe(1);
|
|
500
|
+
|
|
501
|
+
// Second dispose is a no-op.
|
|
502
|
+
harness.client.dispose();
|
|
503
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
504
|
+
expect(harness.disposeCount.count).toBe(1);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("dispose() without any sends does not call connectCdpWsTransport", async () => {
|
|
508
|
+
const harness = createHarness();
|
|
509
|
+
harness.client.dispose();
|
|
510
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
511
|
+
expect(harness.connectCalls).toBe(0);
|
|
512
|
+
expect(harness.disposeCount.count).toBe(0);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("send() after dispose throws CdpError with code 'disposed'", async () => {
|
|
516
|
+
const harness = createHarness();
|
|
517
|
+
harness.client.dispose();
|
|
518
|
+
let caught: unknown;
|
|
519
|
+
try {
|
|
520
|
+
await harness.client.send("Browser.getVersion");
|
|
521
|
+
} catch (err) {
|
|
522
|
+
caught = err;
|
|
523
|
+
}
|
|
524
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
525
|
+
const cdpErr = caught as InstanceType<typeof CdpError>;
|
|
526
|
+
expect(cdpErr.code).toBe("disposed");
|
|
527
|
+
expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
|
|
528
|
+
// No discovery or transport activity took place.
|
|
529
|
+
expect(harness.probeCalls).toBe(0);
|
|
530
|
+
expect(harness.listCalls).toBe(0);
|
|
531
|
+
expect(harness.connectCalls).toBe(0);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("attach that returns no sessionId throws 'cdp_error'", async () => {
|
|
535
|
+
const harness = createHarness({
|
|
536
|
+
transportOnSend: async (method) => {
|
|
537
|
+
if (method === "Target.attachToTarget") {
|
|
538
|
+
// Missing sessionId field — a broken fork response.
|
|
539
|
+
return {};
|
|
540
|
+
}
|
|
541
|
+
return undefined;
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
let caught: unknown;
|
|
545
|
+
try {
|
|
546
|
+
await harness.client.send("Browser.getVersion");
|
|
547
|
+
} catch (err) {
|
|
548
|
+
caught = err;
|
|
549
|
+
}
|
|
550
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
551
|
+
const cdpErr = caught as InstanceType<typeof CdpError>;
|
|
552
|
+
expect(cdpErr.code).toBe("cdp_error");
|
|
553
|
+
expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("attach failure tears down the partially-opened transport", async () => {
|
|
557
|
+
const localDisposeCount = { count: 0 };
|
|
558
|
+
const transport = createFakeTransport({
|
|
559
|
+
onSend: async (method) => {
|
|
560
|
+
if (method === "Target.attachToTarget") {
|
|
561
|
+
throw new CdpWsTransportError("cdp_error", "attach failed", {
|
|
562
|
+
cdpMethod: method,
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
return undefined;
|
|
566
|
+
},
|
|
567
|
+
trackDisposeCount: localDisposeCount,
|
|
568
|
+
});
|
|
569
|
+
const client = createCdpInspectClient("conv-attach-fail", {
|
|
570
|
+
host: "127.0.0.1",
|
|
571
|
+
port: 9222,
|
|
572
|
+
helpers: {
|
|
573
|
+
probeDevToolsJsonVersion: async () => ({
|
|
574
|
+
browser: "Chrome/125.0.0.0",
|
|
575
|
+
protocolVersion: "1.3",
|
|
576
|
+
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
|
|
577
|
+
}),
|
|
578
|
+
listDevToolsTargets: async () => [
|
|
579
|
+
{
|
|
580
|
+
id: "target-1",
|
|
581
|
+
type: "page",
|
|
582
|
+
title: "Example",
|
|
583
|
+
url: "https://example.com/",
|
|
584
|
+
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/target-1",
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
connectCdpWsTransport: async () => transport,
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
let caught: unknown;
|
|
592
|
+
try {
|
|
593
|
+
await client.send("Browser.getVersion");
|
|
594
|
+
} catch (err) {
|
|
595
|
+
caught = err;
|
|
596
|
+
}
|
|
597
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
598
|
+
// The transport opened by attach() should have been disposed so
|
|
599
|
+
// the socket doesn't leak.
|
|
600
|
+
expect(localDisposeCount.count).toBe(1);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("send() aborts promptly when signal fires during ensureSession", async () => {
|
|
604
|
+
// Discovery is deliberately stalled so the caller has to rely on
|
|
605
|
+
// raceAbort() to cut through. If raceAbort worked correctly, the
|
|
606
|
+
// send() promise rejects with an 'aborted' CdpError the instant
|
|
607
|
+
// the controller fires — even though probeDevToolsJsonVersion is
|
|
608
|
+
// still hanging on an unresolved await.
|
|
609
|
+
const controller = new AbortController();
|
|
610
|
+
let probeSignalSeen: AbortSignal | undefined;
|
|
611
|
+
let probeResolve: (() => void) | undefined;
|
|
612
|
+
const probeStarted = new Promise<void>((resolve) => {
|
|
613
|
+
probeResolve = resolve;
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const client = createCdpInspectClient("conv-abort-during-ensure", {
|
|
617
|
+
host: "127.0.0.1",
|
|
618
|
+
port: 9222,
|
|
619
|
+
helpers: {
|
|
620
|
+
probeDevToolsJsonVersion: async (probeOpts) => {
|
|
621
|
+
// Capture the signal so we can assert the shared attach
|
|
622
|
+
// controller actually fired downstream.
|
|
623
|
+
probeSignalSeen = (probeOpts as { signal?: AbortSignal }).signal;
|
|
624
|
+
probeResolve?.();
|
|
625
|
+
// Hang forever unless the shared controller fires.
|
|
626
|
+
await new Promise<never>((_, reject) => {
|
|
627
|
+
const onAbort = () => {
|
|
628
|
+
reject(new Error("probe aborted via shared controller"));
|
|
629
|
+
};
|
|
630
|
+
if (probeSignalSeen?.aborted) {
|
|
631
|
+
onAbort();
|
|
632
|
+
} else {
|
|
633
|
+
probeSignalSeen?.addEventListener("abort", onAbort, {
|
|
634
|
+
once: true,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
throw new Error("unreachable");
|
|
639
|
+
},
|
|
640
|
+
listDevToolsTargets: async () => [
|
|
641
|
+
{
|
|
642
|
+
id: "target-1",
|
|
643
|
+
type: "page",
|
|
644
|
+
title: "Example",
|
|
645
|
+
url: "https://example.com/",
|
|
646
|
+
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/page/target-1",
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
connectCdpWsTransport: async () =>
|
|
650
|
+
createFakeTransport({
|
|
651
|
+
onSend: async (method) => {
|
|
652
|
+
if (method === "Target.attachToTarget") {
|
|
653
|
+
return { sessionId: "fake-session-id" };
|
|
654
|
+
}
|
|
655
|
+
return undefined;
|
|
656
|
+
},
|
|
657
|
+
}),
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const sendPromise = client.send(
|
|
662
|
+
"Browser.getVersion",
|
|
663
|
+
undefined,
|
|
664
|
+
controller.signal,
|
|
665
|
+
);
|
|
666
|
+
// Wait until probe is actually running, then abort.
|
|
667
|
+
await probeStarted;
|
|
668
|
+
controller.abort();
|
|
669
|
+
|
|
670
|
+
let caught: unknown;
|
|
671
|
+
try {
|
|
672
|
+
await sendPromise;
|
|
673
|
+
} catch (err) {
|
|
674
|
+
caught = err;
|
|
675
|
+
}
|
|
676
|
+
expect(caught).toBeInstanceOf(CdpError);
|
|
677
|
+
const cdpErr = caught as InstanceType<typeof CdpError>;
|
|
678
|
+
expect(cdpErr.code).toBe("aborted");
|
|
679
|
+
expect(cdpErr.cdpMethod).toBe("Browser.getVersion");
|
|
680
|
+
// Probe saw a non-null signal, meaning ensureSession plumbed one
|
|
681
|
+
// all the way down to the discovery helper.
|
|
682
|
+
expect(probeSignalSeen).toBeDefined();
|
|
683
|
+
// After the last (and only) waiter aborted, the shared controller
|
|
684
|
+
// should have been aborted — downstream probe's await should have
|
|
685
|
+
// been rejected.
|
|
686
|
+
expect(probeSignalSeen?.aborted).toBe(true);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
test("a new send() after all waiters abort starts a fresh attach", async () => {
|
|
690
|
+
// Regression test for the race condition where onAbort aborts
|
|
691
|
+
// the shared controller but `this.pending` is only cleared later
|
|
692
|
+
// via the async `.catch()` handler in startAttach(). A new caller
|
|
693
|
+
// entering ensureSession() between those two events would reuse
|
|
694
|
+
// the already-aborted pending attach and immediately fail with an
|
|
695
|
+
// `aborted` error even though it never aborted its own signal.
|
|
696
|
+
//
|
|
697
|
+
// Fix: onAbort now clears `this.pending` synchronously BEFORE
|
|
698
|
+
// firing the shared controller.abort(), so any new caller after
|
|
699
|
+
// the abort starts a fresh attach.
|
|
700
|
+
let probeCount = 0;
|
|
701
|
+
let listCount = 0;
|
|
702
|
+
let connectCount = 0;
|
|
703
|
+
|
|
704
|
+
// The first probe hangs until the shared controller aborts it.
|
|
705
|
+
// The second probe (from the fresh attach) resolves normally.
|
|
706
|
+
const firstProbeStarted = Promise.withResolvers<void>();
|
|
707
|
+
|
|
708
|
+
const client = createCdpInspectClient("conv-race", {
|
|
709
|
+
host: "127.0.0.1",
|
|
710
|
+
port: 9222,
|
|
711
|
+
helpers: {
|
|
712
|
+
probeDevToolsJsonVersion: async (probeOpts) => {
|
|
713
|
+
probeCount += 1;
|
|
714
|
+
const signal = (probeOpts as { signal?: AbortSignal }).signal;
|
|
715
|
+
if (probeCount === 1) {
|
|
716
|
+
firstProbeStarted.resolve();
|
|
717
|
+
// Stall until the shared controller aborts us.
|
|
718
|
+
await new Promise<never>((_, reject) => {
|
|
719
|
+
const onAbort = () => {
|
|
720
|
+
reject(new Error("probe aborted via shared controller"));
|
|
721
|
+
};
|
|
722
|
+
if (signal?.aborted) {
|
|
723
|
+
onAbort();
|
|
724
|
+
} else {
|
|
725
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
throw new Error("unreachable");
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
browser: "Chrome/125.0.0.0",
|
|
732
|
+
protocolVersion: "1.3",
|
|
733
|
+
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
|
|
734
|
+
};
|
|
735
|
+
},
|
|
736
|
+
listDevToolsTargets: async () => {
|
|
737
|
+
listCount += 1;
|
|
738
|
+
return [
|
|
739
|
+
{
|
|
740
|
+
id: "target-1",
|
|
741
|
+
type: "page",
|
|
742
|
+
title: "Example",
|
|
743
|
+
url: "https://example.com/",
|
|
744
|
+
webSocketDebuggerUrl:
|
|
745
|
+
"ws://127.0.0.1:9222/devtools/page/target-1",
|
|
746
|
+
},
|
|
747
|
+
];
|
|
748
|
+
},
|
|
749
|
+
connectCdpWsTransport: async () => {
|
|
750
|
+
connectCount += 1;
|
|
751
|
+
return createFakeTransport({
|
|
752
|
+
onSend: async (method) => {
|
|
753
|
+
if (method === "Target.attachToTarget") {
|
|
754
|
+
return { sessionId: "session-race" };
|
|
755
|
+
}
|
|
756
|
+
return { ok: true };
|
|
757
|
+
},
|
|
758
|
+
});
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
// 1. First caller kicks off the attach with signal A.
|
|
764
|
+
const signalA = new AbortController();
|
|
765
|
+
const firstSend = client.send("Runtime.enable", undefined, signalA.signal);
|
|
766
|
+
|
|
767
|
+
// 2. Wait until the first probe has actually started so we know
|
|
768
|
+
// the attach is in-flight and signal A is the only waiter.
|
|
769
|
+
await firstProbeStarted.promise;
|
|
770
|
+
|
|
771
|
+
// 3. Abort signal A. Because it's the only waiter, onAbort will
|
|
772
|
+
// fire the shared controller and (with the fix) clear
|
|
773
|
+
// `this.pending` synchronously.
|
|
774
|
+
signalA.abort();
|
|
775
|
+
|
|
776
|
+
// First caller should reject with `aborted`.
|
|
777
|
+
let firstErr: unknown;
|
|
778
|
+
try {
|
|
779
|
+
await firstSend;
|
|
780
|
+
} catch (err) {
|
|
781
|
+
firstErr = err;
|
|
782
|
+
}
|
|
783
|
+
expect(firstErr).toBeInstanceOf(CdpError);
|
|
784
|
+
expect((firstErr as InstanceType<typeof CdpError>).code).toBe("aborted");
|
|
785
|
+
|
|
786
|
+
// At this point, with the fix, `this.pending` has been cleared
|
|
787
|
+
// synchronously — but without the fix, it would still be set to
|
|
788
|
+
// the aborted pending until the `.catch()` handler in
|
|
789
|
+
// startAttach() runs asynchronously. We intentionally do NOT
|
|
790
|
+
// flush microtasks here before kicking off the second send() so
|
|
791
|
+
// that we exercise the race window.
|
|
792
|
+
//
|
|
793
|
+
// 4. New send() with its own signal B. With the fix, this should
|
|
794
|
+
// start a fresh attach and complete successfully. Without the
|
|
795
|
+
// fix, it would reuse the aborted pending and fail with an
|
|
796
|
+
// `aborted` error even though signal B was never aborted.
|
|
797
|
+
const signalB = new AbortController();
|
|
798
|
+
const secondSend = client.send("Page.enable", undefined, signalB.signal);
|
|
799
|
+
|
|
800
|
+
// Second caller should succeed.
|
|
801
|
+
const secondResult = await secondSend;
|
|
802
|
+
expect(secondResult).toEqual({ ok: true });
|
|
803
|
+
|
|
804
|
+
// 5. Assert that the second send() kicked off a fresh attach —
|
|
805
|
+
// probe, list, and connect should all have been called twice.
|
|
806
|
+
expect(probeCount).toBe(2);
|
|
807
|
+
expect(listCount).toBe(1);
|
|
808
|
+
expect(connectCount).toBe(1);
|
|
809
|
+
// listCount and connectCount are 1 because the first attach
|
|
810
|
+
// aborted during the probe stage — it never reached list or
|
|
811
|
+
// connect. The second (fresh) attach ran probe + list + connect
|
|
812
|
+
// all once.
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
test("concurrent send() callers can abort independently", async () => {
|
|
816
|
+
// Two callers race the same in-flight attach. The first caller
|
|
817
|
+
// aborts; the second caller must still complete normally once the
|
|
818
|
+
// shared attach resolves.
|
|
819
|
+
const aborter = new AbortController();
|
|
820
|
+
let probeResolve: (() => void) | undefined;
|
|
821
|
+
let releaseProbe: (() => void) | undefined;
|
|
822
|
+
const probeRunning = new Promise<void>((resolve) => {
|
|
823
|
+
probeResolve = resolve;
|
|
824
|
+
});
|
|
825
|
+
const probeCanFinish = new Promise<void>((resolve) => {
|
|
826
|
+
releaseProbe = resolve;
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
const harness = createHarness({
|
|
830
|
+
probeImpl: async () => {
|
|
831
|
+
probeResolve?.();
|
|
832
|
+
await probeCanFinish;
|
|
833
|
+
return {
|
|
834
|
+
browser: "Chrome/125.0.0.0",
|
|
835
|
+
protocolVersion: "1.3",
|
|
836
|
+
webSocketDebuggerUrl: "ws://127.0.0.1:9222/devtools/browser/fake",
|
|
837
|
+
};
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
const aborted = harness.client.send(
|
|
842
|
+
"Runtime.enable",
|
|
843
|
+
undefined,
|
|
844
|
+
aborter.signal,
|
|
845
|
+
);
|
|
846
|
+
const stable = harness.client.send("Page.enable");
|
|
847
|
+
|
|
848
|
+
await probeRunning;
|
|
849
|
+
aborter.abort();
|
|
850
|
+
|
|
851
|
+
// First caller aborts promptly…
|
|
852
|
+
let firstErr: unknown;
|
|
853
|
+
try {
|
|
854
|
+
await aborted;
|
|
855
|
+
} catch (err) {
|
|
856
|
+
firstErr = err;
|
|
857
|
+
}
|
|
858
|
+
expect(firstErr).toBeInstanceOf(CdpError);
|
|
859
|
+
expect((firstErr as InstanceType<typeof CdpError>).code).toBe("aborted");
|
|
860
|
+
|
|
861
|
+
// …but the second caller is still alive and can make progress
|
|
862
|
+
// once the shared attach finishes.
|
|
863
|
+
releaseProbe?.();
|
|
864
|
+
await stable;
|
|
865
|
+
expect(harness.probeCalls).toBe(1);
|
|
866
|
+
expect(harness.listCalls).toBe(1);
|
|
867
|
+
expect(harness.connectCalls).toBe(1);
|
|
868
|
+
expect(harness.attachCallCount()).toBe(1);
|
|
869
|
+
});
|
|
870
|
+
});
|