@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,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DevTools HTTP discovery helpers for the `cdp-inspect` backend.
|
|
3
|
+
*
|
|
4
|
+
* These helpers are pure HTTP/domain logic — they do not own a
|
|
5
|
+
* websocket transport, a session manager, or any CDP command state.
|
|
6
|
+
* They exist so the higher-level `cdp-inspect` client can:
|
|
7
|
+
*
|
|
8
|
+
* 1. Probe `/json/version` to verify that a loopback port is actually
|
|
9
|
+
* a Chrome/Chromium DevTools endpoint (and not some other service
|
|
10
|
+
* that happens to be listening).
|
|
11
|
+
* 2. Enumerate available page targets via `/json/list`.
|
|
12
|
+
* 3. Pick a sensible default target when the caller doesn't specify
|
|
13
|
+
* one explicitly.
|
|
14
|
+
*
|
|
15
|
+
* Safety boundary: **only loopback hosts are allowed**. A non-loopback
|
|
16
|
+
* host is rejected *before* any network I/O so that this module can
|
|
17
|
+
* never be coerced into making cross-origin requests on behalf of an
|
|
18
|
+
* attacker-controlled config value.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Stable error codes surfaced by discovery helpers.
|
|
23
|
+
*
|
|
24
|
+
* Callers branch on these codes instead of string-matching messages so
|
|
25
|
+
* upstream UX (status bar, toasts, logs) can render a stable, localized
|
|
26
|
+
* explanation.
|
|
27
|
+
*/
|
|
28
|
+
export type DevToolsDiscoveryErrorCode =
|
|
29
|
+
| "unreachable"
|
|
30
|
+
| "non_loopback"
|
|
31
|
+
| "non_chrome"
|
|
32
|
+
| "invalid_response"
|
|
33
|
+
| "no_targets"
|
|
34
|
+
| "timeout";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Single error type thrown by all discovery helpers. Mirrors the
|
|
38
|
+
* shape of {@link import("../errors.js").CdpError} so catch sites can
|
|
39
|
+
* rely on an explicit `code` field and an optional underlying cause.
|
|
40
|
+
*/
|
|
41
|
+
export class DevToolsDiscoveryError extends Error {
|
|
42
|
+
constructor(
|
|
43
|
+
public readonly code: DevToolsDiscoveryErrorCode,
|
|
44
|
+
message: string,
|
|
45
|
+
public readonly cause?: unknown,
|
|
46
|
+
) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.name = "DevToolsDiscoveryError";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Normalized `/json/version` payload. Chrome returns canonical field
|
|
54
|
+
* names like `Browser` and `Protocol-Version`, but some forks and
|
|
55
|
+
* tests prefer camelCase. The parser here accepts both and normalizes
|
|
56
|
+
* to the camelCase shape below.
|
|
57
|
+
*/
|
|
58
|
+
export interface DevToolsVersionInfo {
|
|
59
|
+
/** Normalized from `"Browser"` or `"browser"`. */
|
|
60
|
+
browser: string;
|
|
61
|
+
/** Normalized from `"Protocol-Version"` or `"protocolVersion"`. */
|
|
62
|
+
protocolVersion: string;
|
|
63
|
+
/** WebSocket URL for the browser-level debugger endpoint. */
|
|
64
|
+
webSocketDebuggerUrl: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A DevTools page target as returned by `/json/list`, filtered down to
|
|
69
|
+
* the fields the cdp-inspect backend actually needs.
|
|
70
|
+
*/
|
|
71
|
+
export interface DevToolsTarget {
|
|
72
|
+
id: string;
|
|
73
|
+
type: string;
|
|
74
|
+
title: string;
|
|
75
|
+
url: string;
|
|
76
|
+
webSocketDebuggerUrl: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Loopback allowlist (exact match, case-insensitive). Any host not in
|
|
81
|
+
* this list is rejected *before* we touch the network.
|
|
82
|
+
*
|
|
83
|
+
* We intentionally do not resolve DNS here — if a config ever gains a
|
|
84
|
+
* hostname that happens to resolve to 127.0.0.1, we still refuse it,
|
|
85
|
+
* because DNS rebinding attacks can flip that answer between the
|
|
86
|
+
* pre-check and the actual fetch.
|
|
87
|
+
*/
|
|
88
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
89
|
+
|
|
90
|
+
function assertLoopback(host: string): void {
|
|
91
|
+
const normalized = host.toLowerCase();
|
|
92
|
+
if (!LOOPBACK_HOSTS.has(normalized)) {
|
|
93
|
+
throw new DevToolsDiscoveryError(
|
|
94
|
+
"non_loopback",
|
|
95
|
+
`Refusing to probe non-loopback DevTools host "${host}". Only loopback hosts (localhost, 127.0.0.1, ::1) are permitted.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate that a `webSocketDebuggerUrl` points to a loopback host.
|
|
102
|
+
*
|
|
103
|
+
* Chrome returns this URL in `/json/version` and `/json/list` responses.
|
|
104
|
+
* A rogue responder on the loopback port could return a ws:// URL
|
|
105
|
+
* pointing to an attacker-controlled host, tricking the client into
|
|
106
|
+
* opening a cross-origin WebSocket. This check ensures the hostname
|
|
107
|
+
* extracted from the URL is in the {@link LOOPBACK_HOSTS} allowlist.
|
|
108
|
+
*/
|
|
109
|
+
function assertWsUrlLoopback(wsUrl: string): void {
|
|
110
|
+
let url: URL;
|
|
111
|
+
try {
|
|
112
|
+
url = new URL(wsUrl);
|
|
113
|
+
} catch {
|
|
114
|
+
throw new DevToolsDiscoveryError(
|
|
115
|
+
"invalid_response",
|
|
116
|
+
`webSocketDebuggerUrl is not a valid URL: ${wsUrl}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const hostname = url.hostname.toLowerCase();
|
|
121
|
+
if (!LOOPBACK_HOSTS.has(hostname)) {
|
|
122
|
+
throw new DevToolsDiscoveryError(
|
|
123
|
+
"non_loopback",
|
|
124
|
+
`webSocketDebuggerUrl host "${url.hostname}" is not loopback. A rogue responder may be redirecting the connection.`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build a fetch-ready loopback URL. `host` is assumed to have already
|
|
131
|
+
* been validated by {@link assertLoopback}. IPv6 bare form (`::1`) is
|
|
132
|
+
* wrapped in square brackets for URL correctness.
|
|
133
|
+
*/
|
|
134
|
+
function buildUrl(host: string, port: number, pathname: string): string {
|
|
135
|
+
const normalized = host.toLowerCase();
|
|
136
|
+
const hostSegment = normalized === "::1" ? "[::1]" : normalized;
|
|
137
|
+
return `http://${hostSegment}:${port}${pathname}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Timeout controller handle returned by {@link withTimeout}. `cleanup`
|
|
142
|
+
* must be called in a `finally` block to avoid leaking the timer and
|
|
143
|
+
* the abort listener. `timedOut` is flipped to `true` if the timer
|
|
144
|
+
* fires before `cleanup` runs.
|
|
145
|
+
*/
|
|
146
|
+
interface TimeoutHandle {
|
|
147
|
+
signal: AbortSignal;
|
|
148
|
+
cleanup: () => void;
|
|
149
|
+
readonly timedOut: boolean;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Merge the caller's signal (if any) with a freshly-minted timeout
|
|
154
|
+
* controller. The flag on the returned handle is the single source of
|
|
155
|
+
* truth for "timed out vs. aborted vs. network error" — we can't
|
|
156
|
+
* recover that distinction from the fetch rejection alone.
|
|
157
|
+
*/
|
|
158
|
+
function withTimeout(
|
|
159
|
+
timeoutMs: number,
|
|
160
|
+
callerSignal?: AbortSignal,
|
|
161
|
+
): TimeoutHandle {
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
let timedOut = false;
|
|
164
|
+
const timer = setTimeout(() => {
|
|
165
|
+
timedOut = true;
|
|
166
|
+
controller.abort(new Error("timeout"));
|
|
167
|
+
}, timeoutMs);
|
|
168
|
+
|
|
169
|
+
let onCallerAbort: (() => void) | null = null;
|
|
170
|
+
if (callerSignal) {
|
|
171
|
+
if (callerSignal.aborted) {
|
|
172
|
+
controller.abort(callerSignal.reason);
|
|
173
|
+
} else {
|
|
174
|
+
onCallerAbort = () => controller.abort(callerSignal.reason);
|
|
175
|
+
callerSignal.addEventListener("abort", onCallerAbort, { once: true });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
signal: controller.signal,
|
|
181
|
+
get timedOut() {
|
|
182
|
+
return timedOut;
|
|
183
|
+
},
|
|
184
|
+
cleanup: () => {
|
|
185
|
+
clearTimeout(timer);
|
|
186
|
+
if (onCallerAbort && callerSignal) {
|
|
187
|
+
callerSignal.removeEventListener("abort", onCallerAbort);
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Classify a `fetch()` rejection into a stable discovery code. We
|
|
195
|
+
* intentionally do not depend on Node's error code strings here — the
|
|
196
|
+
* fetch implementation varies between Bun, Node, and undici — so we
|
|
197
|
+
* look at the name, the message, and the caller-visible abort state
|
|
198
|
+
* instead.
|
|
199
|
+
*/
|
|
200
|
+
function classifyFetchError(
|
|
201
|
+
err: unknown,
|
|
202
|
+
timedOut: boolean,
|
|
203
|
+
callerAborted: boolean,
|
|
204
|
+
): DevToolsDiscoveryError {
|
|
205
|
+
if (callerAborted) {
|
|
206
|
+
return new DevToolsDiscoveryError(
|
|
207
|
+
"unreachable",
|
|
208
|
+
"Discovery fetch was aborted by the caller before completion.",
|
|
209
|
+
err,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (timedOut) {
|
|
213
|
+
return new DevToolsDiscoveryError(
|
|
214
|
+
"timeout",
|
|
215
|
+
"Timed out waiting for DevTools HTTP response.",
|
|
216
|
+
err,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
221
|
+
const name = err instanceof Error ? err.name : "";
|
|
222
|
+
if (name === "AbortError" || /aborted/i.test(message)) {
|
|
223
|
+
// Unspecified abort — most likely the timeout fired but the flag
|
|
224
|
+
// was not flipped yet. Treat as timeout to give the user the
|
|
225
|
+
// clearer message.
|
|
226
|
+
return new DevToolsDiscoveryError(
|
|
227
|
+
"timeout",
|
|
228
|
+
"Timed out waiting for DevTools HTTP response.",
|
|
229
|
+
err,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return new DevToolsDiscoveryError(
|
|
234
|
+
"unreachable",
|
|
235
|
+
`Failed to reach DevTools endpoint: ${message}`,
|
|
236
|
+
err,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Read the response body as text, propagating abort/timeout errors so
|
|
242
|
+
* the caller can distinguish them from a merely malformed payload.
|
|
243
|
+
*
|
|
244
|
+
* The fetch signal is the same AbortSignal passed to the original
|
|
245
|
+
* `fetch()` call, so if the timeout fires (or the caller aborts) while
|
|
246
|
+
* we're still reading the body, the underlying stream is cancelled and
|
|
247
|
+
* `response.text()` rejects. We rethrow those abort-shaped failures as
|
|
248
|
+
* a distinct sentinel (`TimeoutDuringBodyReadError`) so the caller can
|
|
249
|
+
* map them to `timeout` / `unreachable` instead of `invalid_response`.
|
|
250
|
+
*/
|
|
251
|
+
class TimeoutDuringBodyReadError extends Error {
|
|
252
|
+
constructor(
|
|
253
|
+
public readonly timedOut: boolean,
|
|
254
|
+
public readonly callerAborted: boolean,
|
|
255
|
+
public readonly underlying: unknown,
|
|
256
|
+
) {
|
|
257
|
+
super("Discovery body read aborted.");
|
|
258
|
+
this.name = "TimeoutDuringBodyReadError";
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isAbortShaped(err: unknown): boolean {
|
|
263
|
+
if (!(err instanceof Error)) return false;
|
|
264
|
+
if (err.name === "AbortError") return true;
|
|
265
|
+
if (/abort/i.test(err.message)) return true;
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function readResponseText(
|
|
270
|
+
response: Response,
|
|
271
|
+
handle: TimeoutHandle,
|
|
272
|
+
callerSignal: AbortSignal | undefined,
|
|
273
|
+
): Promise<string> {
|
|
274
|
+
try {
|
|
275
|
+
return await response.text();
|
|
276
|
+
} catch (err) {
|
|
277
|
+
const callerAborted = callerSignal?.aborted === true;
|
|
278
|
+
if (handle.timedOut || callerAborted || isAbortShaped(err)) {
|
|
279
|
+
throw new TimeoutDuringBodyReadError(handle.timedOut, callerAborted, err);
|
|
280
|
+
}
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Best-effort JSON parser. Returns a parsed object or throws a
|
|
287
|
+
* discovery error with `invalid_response` so the caller doesn't need
|
|
288
|
+
* to wrap this itself. Assumes the body text has already been read
|
|
289
|
+
* (see {@link readResponseText}).
|
|
290
|
+
*/
|
|
291
|
+
function parseJsonText(text: string, endpoint: string): unknown {
|
|
292
|
+
try {
|
|
293
|
+
return JSON.parse(text);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
throw new DevToolsDiscoveryError(
|
|
296
|
+
"invalid_response",
|
|
297
|
+
`Expected JSON from ${endpoint} but got: ${text.slice(0, 200)}`,
|
|
298
|
+
err,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Pull a string field out of an arbitrary JSON object, supporting
|
|
305
|
+
* either of two casings (e.g. `"Browser"` and `"browser"`). Returns
|
|
306
|
+
* `undefined` if neither key exists or the value is not a string.
|
|
307
|
+
*/
|
|
308
|
+
function readStringField(
|
|
309
|
+
obj: Record<string, unknown>,
|
|
310
|
+
...keys: string[]
|
|
311
|
+
): string | undefined {
|
|
312
|
+
for (const key of keys) {
|
|
313
|
+
const value = obj[key];
|
|
314
|
+
if (typeof value === "string" && value.length > 0) {
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Probe `/json/version` on a loopback DevTools endpoint and return a
|
|
323
|
+
* normalized {@link DevToolsVersionInfo}.
|
|
324
|
+
*
|
|
325
|
+
* Failure modes:
|
|
326
|
+
*
|
|
327
|
+
* - `non_loopback`: host is not one of {localhost, 127.0.0.1, ::1, [::1]}.
|
|
328
|
+
* Raised *before* any network I/O.
|
|
329
|
+
* - `unreachable`: network error, connection refused, DNS failure, etc.
|
|
330
|
+
* - `timeout`: no response within `timeoutMs`.
|
|
331
|
+
* - `invalid_response`: HTTP status != 200, non-JSON body, or missing
|
|
332
|
+
* required fields.
|
|
333
|
+
* - `non_chrome`: the responder does not identify itself as Chrome or
|
|
334
|
+
* Chromium. Guards against e.g. a dev server happening to listen on
|
|
335
|
+
* port 9222.
|
|
336
|
+
*/
|
|
337
|
+
export async function probeDevToolsJsonVersion(opts: {
|
|
338
|
+
host: string;
|
|
339
|
+
port: number;
|
|
340
|
+
timeoutMs: number;
|
|
341
|
+
signal?: AbortSignal;
|
|
342
|
+
}): Promise<DevToolsVersionInfo> {
|
|
343
|
+
assertLoopback(opts.host);
|
|
344
|
+
|
|
345
|
+
const url = buildUrl(opts.host, opts.port, "/json/version");
|
|
346
|
+
const handle = withTimeout(opts.timeoutMs, opts.signal);
|
|
347
|
+
|
|
348
|
+
// Keep `handle` active until AFTER the body has been read, so the
|
|
349
|
+
// timeout (and caller abort) still enforce against a server that
|
|
350
|
+
// stalls mid-body. If we cleared the timer right after `fetch()`
|
|
351
|
+
// resolved, a chunked response that stalls on the second chunk would
|
|
352
|
+
// block `response.text()` indefinitely.
|
|
353
|
+
let text: string;
|
|
354
|
+
try {
|
|
355
|
+
let response: Response;
|
|
356
|
+
try {
|
|
357
|
+
response = await fetch(url, { signal: handle.signal });
|
|
358
|
+
} catch (err) {
|
|
359
|
+
throw classifyFetchError(
|
|
360
|
+
err,
|
|
361
|
+
handle.timedOut,
|
|
362
|
+
opts.signal?.aborted === true,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
throw new DevToolsDiscoveryError(
|
|
368
|
+
"invalid_response",
|
|
369
|
+
`DevTools /json/version returned HTTP ${response.status}.`,
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
text = await readResponseText(response, handle, opts.signal);
|
|
375
|
+
} catch (err) {
|
|
376
|
+
if (err instanceof TimeoutDuringBodyReadError) {
|
|
377
|
+
throw classifyFetchError(
|
|
378
|
+
err.underlying,
|
|
379
|
+
err.timedOut,
|
|
380
|
+
err.callerAborted,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
throw new DevToolsDiscoveryError(
|
|
384
|
+
"invalid_response",
|
|
385
|
+
"Failed to read /json/version response body.",
|
|
386
|
+
err,
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
} finally {
|
|
390
|
+
handle.cleanup();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const parsed = parseJsonText(text, "/json/version");
|
|
394
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
395
|
+
throw new DevToolsDiscoveryError(
|
|
396
|
+
"invalid_response",
|
|
397
|
+
"DevTools /json/version payload is not a JSON object.",
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const record = parsed as Record<string, unknown>;
|
|
402
|
+
const browser = readStringField(record, "Browser", "browser");
|
|
403
|
+
const protocolVersion = readStringField(
|
|
404
|
+
record,
|
|
405
|
+
"Protocol-Version",
|
|
406
|
+
"protocolVersion",
|
|
407
|
+
);
|
|
408
|
+
const webSocketDebuggerUrl = readStringField(
|
|
409
|
+
record,
|
|
410
|
+
"webSocketDebuggerUrl",
|
|
411
|
+
"WebSocketDebuggerUrl",
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (!browser || !protocolVersion || !webSocketDebuggerUrl) {
|
|
415
|
+
throw new DevToolsDiscoveryError(
|
|
416
|
+
"invalid_response",
|
|
417
|
+
"DevTools /json/version payload is missing required fields (browser, protocolVersion, webSocketDebuggerUrl).",
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (!/chrom(e|ium)/i.test(browser)) {
|
|
422
|
+
throw new DevToolsDiscoveryError(
|
|
423
|
+
"non_chrome",
|
|
424
|
+
`DevTools endpoint is not Chrome or Chromium: ${browser}`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
assertWsUrlLoopback(webSocketDebuggerUrl);
|
|
429
|
+
|
|
430
|
+
return { browser, protocolVersion, webSocketDebuggerUrl };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Enumerate `/json/list` and return only usable page targets — i.e.
|
|
435
|
+
* `type === "page"` with a non-empty `webSocketDebuggerUrl`. Throws
|
|
436
|
+
* `no_targets` when the filtered list is empty so the caller doesn't
|
|
437
|
+
* have to decide how to phrase that.
|
|
438
|
+
*
|
|
439
|
+
* Sibling failure modes match {@link probeDevToolsJsonVersion}:
|
|
440
|
+
* `non_loopback`, `unreachable`, `timeout`, `invalid_response`.
|
|
441
|
+
*/
|
|
442
|
+
export async function listDevToolsTargets(opts: {
|
|
443
|
+
host: string;
|
|
444
|
+
port: number;
|
|
445
|
+
timeoutMs: number;
|
|
446
|
+
signal?: AbortSignal;
|
|
447
|
+
}): Promise<DevToolsTarget[]> {
|
|
448
|
+
assertLoopback(opts.host);
|
|
449
|
+
|
|
450
|
+
const url = buildUrl(opts.host, opts.port, "/json/list");
|
|
451
|
+
const handle = withTimeout(opts.timeoutMs, opts.signal);
|
|
452
|
+
|
|
453
|
+
// Keep `handle` active until AFTER the body has been read, so the
|
|
454
|
+
// timeout (and caller abort) still enforce against a server that
|
|
455
|
+
// stalls mid-body. See the matching comment in
|
|
456
|
+
// probeDevToolsJsonVersion for the exact failure mode this guards
|
|
457
|
+
// against.
|
|
458
|
+
let text: string;
|
|
459
|
+
try {
|
|
460
|
+
let response: Response;
|
|
461
|
+
try {
|
|
462
|
+
response = await fetch(url, { signal: handle.signal });
|
|
463
|
+
} catch (err) {
|
|
464
|
+
throw classifyFetchError(
|
|
465
|
+
err,
|
|
466
|
+
handle.timedOut,
|
|
467
|
+
opts.signal?.aborted === true,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (!response.ok) {
|
|
472
|
+
throw new DevToolsDiscoveryError(
|
|
473
|
+
"invalid_response",
|
|
474
|
+
`DevTools /json/list returned HTTP ${response.status}.`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
text = await readResponseText(response, handle, opts.signal);
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (err instanceof TimeoutDuringBodyReadError) {
|
|
482
|
+
throw classifyFetchError(
|
|
483
|
+
err.underlying,
|
|
484
|
+
err.timedOut,
|
|
485
|
+
err.callerAborted,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
throw new DevToolsDiscoveryError(
|
|
489
|
+
"invalid_response",
|
|
490
|
+
"Failed to read /json/list response body.",
|
|
491
|
+
err,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
} finally {
|
|
495
|
+
handle.cleanup();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const parsed = parseJsonText(text, "/json/list");
|
|
499
|
+
if (!Array.isArray(parsed)) {
|
|
500
|
+
throw new DevToolsDiscoveryError(
|
|
501
|
+
"invalid_response",
|
|
502
|
+
"DevTools /json/list payload is not a JSON array.",
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const targets: DevToolsTarget[] = [];
|
|
507
|
+
for (const entry of parsed) {
|
|
508
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const record = entry as Record<string, unknown>;
|
|
512
|
+
const type = readStringField(record, "type");
|
|
513
|
+
if (type !== "page") continue;
|
|
514
|
+
|
|
515
|
+
const webSocketDebuggerUrl = readStringField(
|
|
516
|
+
record,
|
|
517
|
+
"webSocketDebuggerUrl",
|
|
518
|
+
);
|
|
519
|
+
if (!webSocketDebuggerUrl) continue;
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
assertWsUrlLoopback(webSocketDebuggerUrl);
|
|
523
|
+
} catch {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const id = readStringField(record, "id") ?? "";
|
|
528
|
+
const title = readStringField(record, "title") ?? "";
|
|
529
|
+
const targetUrl = readStringField(record, "url") ?? "";
|
|
530
|
+
|
|
531
|
+
targets.push({
|
|
532
|
+
id,
|
|
533
|
+
type,
|
|
534
|
+
title,
|
|
535
|
+
url: targetUrl,
|
|
536
|
+
webSocketDebuggerUrl,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (targets.length === 0) {
|
|
541
|
+
throw new DevToolsDiscoveryError(
|
|
542
|
+
"no_targets",
|
|
543
|
+
"No usable page targets returned by DevTools /json/list.",
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return targets;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Pick a sensible default target from a filtered list. Prefers targets
|
|
552
|
+
* whose URL is not `chrome://`, `devtools://`, or `about:blank`, then
|
|
553
|
+
* falls back to the first entry. Callers that need more specific
|
|
554
|
+
* control should iterate the list themselves.
|
|
555
|
+
*
|
|
556
|
+
* Throws `no_targets` on an empty input list — this mirrors the shape
|
|
557
|
+
* of {@link listDevToolsTargets}, so callers that chain the two can
|
|
558
|
+
* rely on a single error code path.
|
|
559
|
+
*/
|
|
560
|
+
export function pickDefaultTarget(targets: DevToolsTarget[]): DevToolsTarget {
|
|
561
|
+
if (targets.length === 0) {
|
|
562
|
+
throw new DevToolsDiscoveryError(
|
|
563
|
+
"no_targets",
|
|
564
|
+
"pickDefaultTarget called with an empty target list.",
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const preferred = targets.find((target) => !isUtilityTarget(target));
|
|
569
|
+
return preferred ?? targets[0]!;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function isUtilityTarget(target: DevToolsTarget): boolean {
|
|
573
|
+
const url = target.url.toLowerCase();
|
|
574
|
+
if (url.startsWith("chrome://")) return true;
|
|
575
|
+
if (url.startsWith("devtools://")) return true;
|
|
576
|
+
if (url === "about:blank") return true;
|
|
577
|
+
return false;
|
|
578
|
+
}
|