@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
|
@@ -32,6 +32,7 @@ export const SAFE_ENV_VARS = [
|
|
|
32
32
|
"CES_BOOTSTRAP_SOCKET_DIR",
|
|
33
33
|
"GATEWAY_INTERNAL_URL",
|
|
34
34
|
"VELLUM_PLATFORM_URL",
|
|
35
|
+
"VELLUM_DOCS_BASE_URL",
|
|
35
36
|
"CES_CREDENTIAL_URL",
|
|
36
37
|
"CES_MANAGED_MODE",
|
|
37
38
|
"IS_CONTAINERIZED",
|
|
@@ -42,6 +43,8 @@ export const SAFE_ENV_VARS = [
|
|
|
42
43
|
"VELLUM_PROFILER_MAX_BYTES",
|
|
43
44
|
"VELLUM_PROFILER_MAX_RUNS",
|
|
44
45
|
"VELLUM_PROFILER_MIN_FREE_MB",
|
|
46
|
+
"VELLUM_MEMORY_LIMIT",
|
|
47
|
+
"VELLUM_CPU_LIMIT",
|
|
45
48
|
] as const;
|
|
46
49
|
|
|
47
50
|
/**
|
|
@@ -71,5 +74,9 @@ export function buildSanitizedEnv(): Record<string, string> {
|
|
|
71
74
|
// Expose the workspace directory so skills and child processes can read/write
|
|
72
75
|
// workspace-scoped files (e.g. avatar traits, user data).
|
|
73
76
|
env.VELLUM_WORKSPACE_DIR = getWorkspaceDir();
|
|
77
|
+
// Ensure UTF-8 locale so multi-byte characters (em dashes, curly quotes,
|
|
78
|
+
// arrows, etc.) survive piping through tools like pbcopy without corruption.
|
|
79
|
+
if (!env.LANG) env.LANG = "C.UTF-8";
|
|
80
|
+
if (!env.LC_ALL) env.LC_ALL = "C.UTF-8";
|
|
74
81
|
return env;
|
|
75
82
|
}
|
|
@@ -327,30 +327,35 @@ class ShellTool implements Tool {
|
|
|
327
327
|
detached: true,
|
|
328
328
|
});
|
|
329
329
|
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
// Kill the entire process tree. Tries the process group first
|
|
331
|
+
// (negative PID), then falls back to killing the direct child if the
|
|
332
|
+
// PID is unavailable or the group kill fails.
|
|
333
|
+
const killTree = () => {
|
|
334
|
+
if (child.pid != null) {
|
|
335
|
+
try {
|
|
336
|
+
process.kill(-child.pid, "SIGKILL");
|
|
337
|
+
return;
|
|
338
|
+
} catch {
|
|
339
|
+
// Process group may have already exited — fall through.
|
|
340
|
+
}
|
|
341
|
+
}
|
|
332
342
|
try {
|
|
333
|
-
|
|
343
|
+
child.kill("SIGKILL");
|
|
334
344
|
} catch {
|
|
335
|
-
//
|
|
345
|
+
// Child may have already exited.
|
|
336
346
|
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const timer = setTimeout(() => {
|
|
350
|
+
timedOut = true;
|
|
351
|
+
killTree();
|
|
337
352
|
}, timeoutMs);
|
|
338
353
|
|
|
339
354
|
// Cooperative cancellation via AbortSignal
|
|
340
|
-
const onAbort = () =>
|
|
341
|
-
try {
|
|
342
|
-
process.kill(-child.pid!, "SIGKILL");
|
|
343
|
-
} catch {
|
|
344
|
-
// Process group may have already exited.
|
|
345
|
-
}
|
|
346
|
-
};
|
|
355
|
+
const onAbort = () => killTree();
|
|
347
356
|
if (context.signal) {
|
|
348
357
|
if (context.signal.aborted) {
|
|
349
|
-
|
|
350
|
-
process.kill(-child.pid!, "SIGKILL");
|
|
351
|
-
} catch {
|
|
352
|
-
// Process group may have already exited.
|
|
353
|
-
}
|
|
358
|
+
killTree();
|
|
354
359
|
} else {
|
|
355
360
|
context.signal.addEventListener("abort", onAbort, { once: true });
|
|
356
361
|
}
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
getCanonicalGuardianRequest,
|
|
5
5
|
updateCanonicalGuardianRequest,
|
|
6
6
|
} from "../memory/canonical-guardian-store.js";
|
|
7
|
+
import {
|
|
8
|
+
isConversationHostAccessEnabled,
|
|
9
|
+
isPermissionControlsV2Enabled,
|
|
10
|
+
} from "../permissions/v2-consent-policy.js";
|
|
7
11
|
import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
|
|
8
12
|
import { createOrReuseToolGrantRequest } from "../runtime/tool-grant-request-helper.js";
|
|
9
13
|
import { redactSecrets } from "../security/secret-scanner.js";
|
|
@@ -178,6 +182,10 @@ function guardianApprovalDeniedMessage(
|
|
|
178
182
|
return `Permission denied for "${toolName}": this action requires guardian approval and the current actor is not the guardian.`;
|
|
179
183
|
}
|
|
180
184
|
|
|
185
|
+
function trustedContactHostAccessDeniedMessage(toolName: string): string {
|
|
186
|
+
return `Permission denied for "${toolName}": computer access is not enabled for this conversation. Confirm the guardian's intent conversationally and ask them to enable computer access for this conversation before retrying.`;
|
|
187
|
+
}
|
|
188
|
+
|
|
181
189
|
export type PreExecutionGateResult =
|
|
182
190
|
| { allowed: true; tool: Tool; grantConsumed?: boolean }
|
|
183
191
|
| { allowed: false; result: ToolExecutionResult };
|
|
@@ -216,6 +224,8 @@ export class ToolApprovalHandler {
|
|
|
216
224
|
startTime: number,
|
|
217
225
|
emitLifecycleEvent: (event: ToolLifecycleEvent) => void,
|
|
218
226
|
): Promise<PreExecutionGateResult> {
|
|
227
|
+
const v2Enabled = isPermissionControlsV2Enabled();
|
|
228
|
+
|
|
219
229
|
// Bail out immediately if the session was aborted before this tool started.
|
|
220
230
|
if (context.signal?.aborted) {
|
|
221
231
|
const durationMs = Date.now() - startTime;
|
|
@@ -286,9 +296,44 @@ export class ToolApprovalHandler {
|
|
|
286
296
|
| Parameters<typeof consumeGrantForInvocation>[0]
|
|
287
297
|
| null = null;
|
|
288
298
|
|
|
299
|
+
const guardianApprovalRequired = requiresGuardianApprovalForActor(
|
|
300
|
+
name,
|
|
301
|
+
input,
|
|
302
|
+
executionTarget,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (
|
|
306
|
+
v2Enabled &&
|
|
307
|
+
context.trustClass === "trusted_contact" &&
|
|
308
|
+
guardianApprovalRequired &&
|
|
309
|
+
executionTarget === "host" &&
|
|
310
|
+
!isConversationHostAccessEnabled(context.conversationId)
|
|
311
|
+
) {
|
|
312
|
+
const durationMs = Date.now() - startTime;
|
|
313
|
+
const reason = trustedContactHostAccessDeniedMessage(name);
|
|
314
|
+
emitLifecycleEvent({
|
|
315
|
+
type: "permission_denied",
|
|
316
|
+
toolName: name,
|
|
317
|
+
executionTarget,
|
|
318
|
+
input,
|
|
319
|
+
workingDir: context.workingDir,
|
|
320
|
+
conversationId: context.conversationId,
|
|
321
|
+
requestId: context.requestId,
|
|
322
|
+
riskLevel,
|
|
323
|
+
decision: "deny",
|
|
324
|
+
reason,
|
|
325
|
+
durationMs,
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
allowed: false,
|
|
329
|
+
result: { content: reason, isError: true },
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
289
333
|
if (
|
|
290
334
|
isUntrustedTrustClass(context.trustClass) &&
|
|
291
|
-
|
|
335
|
+
guardianApprovalRequired &&
|
|
336
|
+
!(v2Enabled && context.trustClass === "trusted_contact")
|
|
292
337
|
) {
|
|
293
338
|
const inputDigest = computeToolApprovalDigest(name, input);
|
|
294
339
|
needsGrantConsumption = true;
|
|
@@ -513,7 +558,8 @@ export class ToolApprovalHandler {
|
|
|
513
558
|
toolName: name,
|
|
514
559
|
inputDigest,
|
|
515
560
|
questionText: buildToolGrantQuestionText(name, input, context),
|
|
516
|
-
requesterIdentifier:
|
|
561
|
+
requesterIdentifier:
|
|
562
|
+
context.requesterDisplayName || context.requesterIdentifier,
|
|
517
563
|
});
|
|
518
564
|
|
|
519
565
|
// Only wait inline if the escalation succeeded (created or deduped).
|
package/src/tools/types.ts
CHANGED
|
@@ -176,6 +176,8 @@ export interface ToolContext {
|
|
|
176
176
|
toolUseId?: string;
|
|
177
177
|
/** Optional proxy for delegating host_bash execution to a connected client (managed/cloud-hosted mode). */
|
|
178
178
|
hostBashProxy?: import("../daemon/host-bash-proxy.js").HostBashProxy;
|
|
179
|
+
/** Optional proxy for delegating CDP commands to a connected client (managed/cloud-hosted mode). */
|
|
180
|
+
hostBrowserProxy?: import("../daemon/host-browser-proxy.js").HostBrowserProxy;
|
|
179
181
|
/** Optional proxy for delegating host_file_read/write/edit execution to a connected client (managed/cloud-hosted mode). */
|
|
180
182
|
hostFileProxy?: import("../daemon/host-file-proxy.js").HostFileProxy;
|
|
181
183
|
/** True when the assistant is running as a platform-managed remote instance. Used to auto-approve sandboxed bash tools. */
|
package/src/util/platform.ts
CHANGED
|
@@ -136,30 +136,13 @@ export function isTCPEnabled(): boolean {
|
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
138
|
* Returns the hostname/address for the TCP listener.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
139
|
+
* Always binds to localhost only. iOS pairing uses the gateway
|
|
140
|
+
* relay.
|
|
141
141
|
*/
|
|
142
142
|
export function getTCPHost(): string {
|
|
143
|
-
if (isIOSPairingEnabled()) return "0.0.0.0";
|
|
144
143
|
return "127.0.0.1";
|
|
145
144
|
}
|
|
146
145
|
|
|
147
|
-
/**
|
|
148
|
-
* Returns whether iOS pairing mode is enabled.
|
|
149
|
-
* When enabled, the TCP listener binds to 0.0.0.0 (all interfaces)
|
|
150
|
-
* instead of 127.0.0.1 (localhost only), making the daemon reachable
|
|
151
|
-
* from iOS devices on the same local network.
|
|
152
|
-
*
|
|
153
|
-
* Checks for the presence of the flag file ~/.vellum/ios-pairing-enabled.
|
|
154
|
-
* Default: false.
|
|
155
|
-
*
|
|
156
|
-
* This is separate from isTCPEnabled() — TCP can be enabled for localhost-only
|
|
157
|
-
* access without exposing the daemon to the LAN.
|
|
158
|
-
*/
|
|
159
|
-
export function isIOSPairingEnabled(): boolean {
|
|
160
|
-
return existsSync(join(vellumRoot(), "ios-pairing-enabled"));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
146
|
/**
|
|
164
147
|
* Returns the XDG-compliant path for the platform API token
|
|
165
148
|
* (~/.config/vellum/platform-token). This is the canonical location
|
|
@@ -202,6 +185,18 @@ export function getPidPath(): string {
|
|
|
202
185
|
return join(vellumRoot(), "vellum.pid");
|
|
203
186
|
}
|
|
204
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Returns the path to the runtime HTTP port file (~/.vellum/runtime-port).
|
|
190
|
+
* The daemon writes its active HTTP port here on startup so thin helpers
|
|
191
|
+
* that need to reach the runtime (e.g. the chrome-extension native messaging
|
|
192
|
+
* helper) can locate a non-default `RUNTIME_HTTP_PORT` without a manifest
|
|
193
|
+
* change. Root-level path by design — the file is read by helpers that may
|
|
194
|
+
* not know the workspace override path.
|
|
195
|
+
*/
|
|
196
|
+
export function getRuntimePortFilePath(): string {
|
|
197
|
+
return join(vellumRoot(), "runtime-port");
|
|
198
|
+
}
|
|
199
|
+
|
|
205
200
|
export function getDbPath(): string {
|
|
206
201
|
return join(getDataDir(), "db", "assistant.db");
|
|
207
202
|
}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
import { homedir, userInfo } from "node:os";
|
|
2
|
+
|
|
1
3
|
import type { TopLevelSnapshot } from "./top-level-scanner.js";
|
|
2
4
|
|
|
3
5
|
export interface WorkspaceTopLevelRenderOptions {
|
|
4
6
|
conversationAttachmentsPath?: string | null;
|
|
7
|
+
/**
|
|
8
|
+
* Host home directory on the client machine. When provided, takes
|
|
9
|
+
* precedence over the daemon's own `os.homedir()`. This matters for
|
|
10
|
+
* platform-managed (containerized) daemons where `os.homedir()` returns
|
|
11
|
+
* the container's home, not the user's actual Mac.
|
|
12
|
+
*/
|
|
13
|
+
hostHomeDir?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Host username on the client machine. When provided, takes precedence
|
|
16
|
+
* over the daemon's own `os.userInfo().username`. See `hostHomeDir`.
|
|
17
|
+
*/
|
|
18
|
+
hostUsername?: string;
|
|
5
19
|
}
|
|
6
20
|
|
|
7
21
|
/**
|
|
@@ -19,11 +33,15 @@ export function renderWorkspaceTopLevelContext(
|
|
|
19
33
|
lines.push(`Directories: ${snapshot.directories.join(", ")}`);
|
|
20
34
|
lines.push(`Files: ${snapshot.files.join(", ")}`);
|
|
21
35
|
if (options.conversationAttachmentsPath) {
|
|
22
|
-
lines.push(
|
|
36
|
+
lines.push(
|
|
37
|
+
`Current conversation attachments: ${options.conversationAttachmentsPath}`,
|
|
38
|
+
);
|
|
23
39
|
}
|
|
24
40
|
if (snapshot.truncated) {
|
|
25
41
|
lines.push("(list truncated — more entries exist)");
|
|
26
42
|
}
|
|
43
|
+
lines.push(`Host home directory: ${options.hostHomeDir ?? homedir()}`);
|
|
44
|
+
lines.push(`Host username: ${options.hostUsername ?? userInfo().username}`);
|
|
27
45
|
lines.push("</workspace>");
|
|
28
46
|
return lines.join("\n");
|
|
29
47
|
}
|
|
@@ -1,419 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Mocks
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
// We need to mock child_process.spawn and execSync, and the global fetch /
|
|
8
|
-
// WebSocket so tests don't need a real Chrome process.
|
|
9
|
-
|
|
10
|
-
const spawnMock = mock(() => {
|
|
11
|
-
const proc = { unref: mock(() => {}) };
|
|
12
|
-
return proc;
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const execSyncMock = mock(() => "");
|
|
16
|
-
|
|
17
|
-
mock.module("node:child_process", () => ({
|
|
18
|
-
spawn: spawnMock,
|
|
19
|
-
execSync: execSyncMock,
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
const {
|
|
23
|
-
ensureChromeWithCdp,
|
|
24
|
-
isCdpReady,
|
|
25
|
-
minimizeChromeWindow,
|
|
26
|
-
restoreChromeWindow,
|
|
27
|
-
} = await import("../tools/browser/chrome-cdp.js");
|
|
28
|
-
|
|
29
|
-
// Track fetch calls so we can assert readiness-check behavior
|
|
30
|
-
let fetchImpl: (url: string | URL | Request) => Promise<Response>;
|
|
31
|
-
|
|
32
|
-
const originalFetch = globalThis.fetch;
|
|
33
|
-
|
|
34
|
-
/** Helper: a fetchImpl that simulates a CDP endpoint with page targets. */
|
|
35
|
-
function cdpReadyFetch(url: string | URL | Request): Promise<Response> {
|
|
36
|
-
const urlStr = String(url);
|
|
37
|
-
if (urlStr.includes("/json/list")) {
|
|
38
|
-
return Promise.resolve(
|
|
39
|
-
new Response(JSON.stringify([{ type: "page" }]), { status: 200 }),
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
return Promise.resolve(new Response("{}", { status: 200 }));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
// Default: CDP not ready
|
|
47
|
-
fetchImpl = async () => {
|
|
48
|
-
throw new Error("Connection refused");
|
|
49
|
-
};
|
|
50
|
-
globalThis.fetch = (async (
|
|
51
|
-
input: string | URL | Request,
|
|
52
|
-
_init?: RequestInit,
|
|
53
|
-
) => {
|
|
54
|
-
return fetchImpl(input);
|
|
55
|
-
}) as typeof globalThis.fetch;
|
|
56
|
-
spawnMock.mockClear();
|
|
57
|
-
execSyncMock.mockClear();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
afterEach(() => {
|
|
61
|
-
globalThis.fetch = originalFetch;
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// isCdpReady
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
describe("isCdpReady", () => {
|
|
69
|
-
test("returns true when the endpoint responds with 200 and has page targets", async () => {
|
|
70
|
-
fetchImpl = cdpReadyFetch;
|
|
71
|
-
expect(await isCdpReady()).toBe(true);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test("returns false when fetch throws (connection refused)", async () => {
|
|
75
|
-
fetchImpl = async () => {
|
|
76
|
-
throw new Error("Connection refused");
|
|
77
|
-
};
|
|
78
|
-
expect(await isCdpReady()).toBe(false);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("returns false when the endpoint responds with non-ok status", async () => {
|
|
82
|
-
fetchImpl = async () => new Response("", { status: 500 });
|
|
83
|
-
expect(await isCdpReady()).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("returns false when CDP is up but has no page targets", async () => {
|
|
87
|
-
fetchImpl = async (url) => {
|
|
88
|
-
const urlStr = String(url);
|
|
89
|
-
if (urlStr.includes("/json/list")) {
|
|
90
|
-
return new Response("[]", { status: 200 });
|
|
91
|
-
}
|
|
92
|
-
return new Response("{}", { status: 200 });
|
|
93
|
-
};
|
|
94
|
-
expect(await isCdpReady()).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("uses the provided base URL", async () => {
|
|
98
|
-
const calledUrls: string[] = [];
|
|
99
|
-
fetchImpl = async (url) => {
|
|
100
|
-
const urlStr = String(url);
|
|
101
|
-
calledUrls.push(urlStr);
|
|
102
|
-
if (urlStr.includes("/json/list")) {
|
|
103
|
-
return new Response(JSON.stringify([{ type: "page" }]), {
|
|
104
|
-
status: 200,
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
return new Response("{}", { status: 200 });
|
|
108
|
-
};
|
|
109
|
-
await isCdpReady("http://localhost:9333");
|
|
110
|
-
expect(calledUrls.some((u) => u.startsWith("http://localhost:9333/"))).toBe(
|
|
111
|
-
true,
|
|
112
|
-
);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// ---------------------------------------------------------------------------
|
|
117
|
-
// ensureChromeWithCdp
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
|
|
120
|
-
describe("ensureChromeWithCdp", () => {
|
|
121
|
-
test("returns immediately if CDP is already ready (launchedByUs=false)", async () => {
|
|
122
|
-
fetchImpl = cdpReadyFetch;
|
|
123
|
-
const session = await ensureChromeWithCdp();
|
|
124
|
-
expect(session.launchedByUs).toBe(false);
|
|
125
|
-
expect(session.baseUrl).toBe("http://localhost:9222");
|
|
126
|
-
expect(spawnMock).not.toHaveBeenCalled();
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test("spawns Chrome and retries when CDP is not initially ready", async () => {
|
|
130
|
-
let callCount = 0;
|
|
131
|
-
fetchImpl = async (url) => {
|
|
132
|
-
callCount++;
|
|
133
|
-
// First isCdpReady check fails (2 fetch calls: /json/version + /json/list).
|
|
134
|
-
// Stale-check /json/version also fails.
|
|
135
|
-
// After spawn, successive isCdpReady calls succeed on the 5th overall call.
|
|
136
|
-
if (callCount >= 5) {
|
|
137
|
-
const urlStr = String(url);
|
|
138
|
-
if (urlStr.includes("/json/list")) {
|
|
139
|
-
return new Response(JSON.stringify([{ type: "page" }]), {
|
|
140
|
-
status: 200,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
return new Response("{}", { status: 200 });
|
|
144
|
-
}
|
|
145
|
-
throw new Error("Connection refused");
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
// Replace setTimeout with a zero-delay version to skip retry waits
|
|
149
|
-
const origSetTimeout = globalThis.setTimeout;
|
|
150
|
-
globalThis.setTimeout = ((
|
|
151
|
-
fn: TimerHandler,
|
|
152
|
-
_ms?: number,
|
|
153
|
-
...args: unknown[]
|
|
154
|
-
) => {
|
|
155
|
-
return origSetTimeout(fn, 0, ...args);
|
|
156
|
-
}) as typeof setTimeout;
|
|
157
|
-
try {
|
|
158
|
-
const session = await ensureChromeWithCdp({
|
|
159
|
-
startUrl: "https://example.com/",
|
|
160
|
-
});
|
|
161
|
-
expect(session.launchedByUs).toBe(true);
|
|
162
|
-
expect(session.baseUrl).toBe("http://localhost:9222");
|
|
163
|
-
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
164
|
-
|
|
165
|
-
// Verify spawn was called with the right Chrome path and args
|
|
166
|
-
const spawnArgs = spawnMock.mock.calls[0] as unknown as [
|
|
167
|
-
string,
|
|
168
|
-
string[],
|
|
169
|
-
];
|
|
170
|
-
expect(spawnArgs[0]).toContain("Google Chrome");
|
|
171
|
-
const flags = spawnArgs[1];
|
|
172
|
-
expect(flags).toContain("--remote-debugging-port=9222");
|
|
173
|
-
expect(flags).toContain("--force-renderer-accessibility");
|
|
174
|
-
expect(flags.some((f: string) => f.includes("Chrome-CDP"))).toBe(true);
|
|
175
|
-
expect(flags).toContain("https://example.com/");
|
|
176
|
-
} finally {
|
|
177
|
-
globalThis.setTimeout = origSetTimeout;
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("uses custom port when specified", async () => {
|
|
182
|
-
fetchImpl = cdpReadyFetch;
|
|
183
|
-
const session = await ensureChromeWithCdp({ port: 9333 });
|
|
184
|
-
expect(session.baseUrl).toBe("http://localhost:9333");
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
test("uses custom userDataDir when specified", async () => {
|
|
188
|
-
fetchImpl = cdpReadyFetch;
|
|
189
|
-
const session = await ensureChromeWithCdp({
|
|
190
|
-
userDataDir: "/tmp/test-chrome",
|
|
191
|
-
});
|
|
192
|
-
expect(session.userDataDir).toBe("/tmp/test-chrome");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
test("throws after exhausting retries", async () => {
|
|
196
|
-
// Never becomes ready
|
|
197
|
-
fetchImpl = async () => {
|
|
198
|
-
throw new Error("Connection refused");
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
// Replace setTimeout with a zero-delay version to skip retry waits
|
|
202
|
-
const origSetTimeout = globalThis.setTimeout;
|
|
203
|
-
globalThis.setTimeout = ((
|
|
204
|
-
fn: TimerHandler,
|
|
205
|
-
_ms?: number,
|
|
206
|
-
...args: unknown[]
|
|
207
|
-
) => {
|
|
208
|
-
return origSetTimeout(fn, 0, ...args);
|
|
209
|
-
}) as typeof setTimeout;
|
|
210
|
-
try {
|
|
211
|
-
const promise = ensureChromeWithCdp();
|
|
212
|
-
await expect(promise).rejects.toThrow("CDP endpoint not responding");
|
|
213
|
-
} finally {
|
|
214
|
-
globalThis.setTimeout = origSetTimeout;
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// ---------------------------------------------------------------------------
|
|
220
|
-
// Window management (minimize / restore)
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
|
-
|
|
223
|
-
describe("minimizeChromeWindow", () => {
|
|
224
|
-
test("does nothing when no page targets exist", async () => {
|
|
225
|
-
fetchImpl = async () => new Response("[]", { status: 200 });
|
|
226
|
-
// Should not throw
|
|
227
|
-
await minimizeChromeWindow();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test("sends minimize command via WebSocket", async () => {
|
|
231
|
-
// Track the WebSocket interactions
|
|
232
|
-
const sentMessages: string[] = [];
|
|
233
|
-
let wsOnOpen: (() => void) | undefined;
|
|
234
|
-
let wsOnMessage: ((event: { data: string }) => void) | undefined;
|
|
235
|
-
|
|
236
|
-
fetchImpl = async (url) => {
|
|
237
|
-
const urlStr = String(url);
|
|
238
|
-
if (urlStr.includes("/json/list")) {
|
|
239
|
-
return new Response(
|
|
240
|
-
JSON.stringify([
|
|
241
|
-
{
|
|
242
|
-
type: "page",
|
|
243
|
-
webSocketDebuggerUrl: "ws://localhost:9222/devtools/page/ABC",
|
|
244
|
-
},
|
|
245
|
-
]),
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
return new Response("{}", { status: 200 });
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const OriginalWebSocket = globalThis.WebSocket;
|
|
252
|
-
globalThis.WebSocket = class MockWebSocket {
|
|
253
|
-
constructor(_url: string) {}
|
|
254
|
-
addEventListener(event: string, handler: (...args: unknown[]) => void) {
|
|
255
|
-
if (event === "open") wsOnOpen = handler as () => void;
|
|
256
|
-
if (event === "message")
|
|
257
|
-
wsOnMessage = handler as (event: { data: string }) => void;
|
|
258
|
-
}
|
|
259
|
-
send(data: string) {
|
|
260
|
-
sentMessages.push(data);
|
|
261
|
-
const msg = JSON.parse(data);
|
|
262
|
-
if (msg.method === "Browser.getWindowForTarget") {
|
|
263
|
-
// Simulate response with windowId
|
|
264
|
-
setTimeout(() => {
|
|
265
|
-
wsOnMessage?.({
|
|
266
|
-
data: JSON.stringify({ id: 1, result: { windowId: 42 } }),
|
|
267
|
-
});
|
|
268
|
-
}, 0);
|
|
269
|
-
} else if (msg.method === "Browser.setWindowBounds") {
|
|
270
|
-
setTimeout(() => {
|
|
271
|
-
wsOnMessage?.({ data: JSON.stringify({ id: 2, result: {} }) });
|
|
272
|
-
}, 0);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
close() {}
|
|
276
|
-
} as unknown as typeof WebSocket;
|
|
277
|
-
|
|
278
|
-
const promise = minimizeChromeWindow();
|
|
279
|
-
|
|
280
|
-
// Trigger the open event
|
|
281
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
282
|
-
wsOnOpen?.();
|
|
283
|
-
|
|
284
|
-
await promise;
|
|
285
|
-
|
|
286
|
-
// Verify the setWindowBounds call had windowState: "minimized"
|
|
287
|
-
const boundsMsg = sentMessages.find((m) =>
|
|
288
|
-
m.includes("Browser.setWindowBounds"),
|
|
289
|
-
);
|
|
290
|
-
expect(boundsMsg).toBeDefined();
|
|
291
|
-
const parsed = JSON.parse(boundsMsg!);
|
|
292
|
-
expect(parsed.params.bounds.windowState).toBe("minimized");
|
|
293
|
-
|
|
294
|
-
globalThis.WebSocket = OriginalWebSocket;
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
describe("setWindowState error handling", () => {
|
|
299
|
-
test("rejects when Browser.setWindowBounds returns an error", async () => {
|
|
300
|
-
let wsOnOpen: (() => void) | undefined;
|
|
301
|
-
let wsOnMessage: ((event: { data: string }) => void) | undefined;
|
|
302
|
-
|
|
303
|
-
fetchImpl = async (url) => {
|
|
304
|
-
const urlStr = String(url);
|
|
305
|
-
if (urlStr.includes("/json/list")) {
|
|
306
|
-
return new Response(
|
|
307
|
-
JSON.stringify([
|
|
308
|
-
{
|
|
309
|
-
type: "page",
|
|
310
|
-
webSocketDebuggerUrl: "ws://localhost:9222/devtools/page/ABC",
|
|
311
|
-
},
|
|
312
|
-
]),
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
return new Response("{}", { status: 200 });
|
|
316
|
-
};
|
|
317
|
-
|
|
318
|
-
const OriginalWebSocket = globalThis.WebSocket;
|
|
319
|
-
globalThis.WebSocket = class MockWebSocket {
|
|
320
|
-
constructor(_url: string) {}
|
|
321
|
-
addEventListener(event: string, handler: (...args: unknown[]) => void) {
|
|
322
|
-
if (event === "open") wsOnOpen = handler as () => void;
|
|
323
|
-
if (event === "message")
|
|
324
|
-
wsOnMessage = handler as (event: { data: string }) => void;
|
|
325
|
-
}
|
|
326
|
-
send(data: string) {
|
|
327
|
-
const msg = JSON.parse(data);
|
|
328
|
-
if (msg.method === "Browser.getWindowForTarget") {
|
|
329
|
-
setTimeout(() => {
|
|
330
|
-
wsOnMessage?.({
|
|
331
|
-
data: JSON.stringify({ id: 1, result: { windowId: 42 } }),
|
|
332
|
-
});
|
|
333
|
-
}, 0);
|
|
334
|
-
} else if (msg.method === "Browser.setWindowBounds") {
|
|
335
|
-
setTimeout(() => {
|
|
336
|
-
wsOnMessage?.({
|
|
337
|
-
data: JSON.stringify({
|
|
338
|
-
id: 2,
|
|
339
|
-
error: { message: "No window with given id" },
|
|
340
|
-
}),
|
|
341
|
-
});
|
|
342
|
-
}, 0);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
close() {}
|
|
346
|
-
} as unknown as typeof WebSocket;
|
|
347
|
-
|
|
348
|
-
const promise = minimizeChromeWindow();
|
|
349
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
350
|
-
wsOnOpen?.();
|
|
351
|
-
|
|
352
|
-
await expect(promise).rejects.toThrow("Browser.setWindowBounds failed");
|
|
353
|
-
|
|
354
|
-
globalThis.WebSocket = OriginalWebSocket;
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
describe("restoreChromeWindow", () => {
|
|
359
|
-
test("sends restore command via WebSocket", async () => {
|
|
360
|
-
const sentMessages: string[] = [];
|
|
361
|
-
let wsOnOpen: (() => void) | undefined;
|
|
362
|
-
let wsOnMessage: ((event: { data: string }) => void) | undefined;
|
|
363
|
-
|
|
364
|
-
fetchImpl = async (url) => {
|
|
365
|
-
const urlStr = String(url);
|
|
366
|
-
if (urlStr.includes("/json/list")) {
|
|
367
|
-
return new Response(
|
|
368
|
-
JSON.stringify([
|
|
369
|
-
{
|
|
370
|
-
type: "page",
|
|
371
|
-
webSocketDebuggerUrl: "ws://localhost:9222/devtools/page/ABC",
|
|
372
|
-
},
|
|
373
|
-
]),
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
return new Response("{}", { status: 200 });
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
const OriginalWebSocket = globalThis.WebSocket;
|
|
380
|
-
globalThis.WebSocket = class MockWebSocket {
|
|
381
|
-
constructor(_url: string) {}
|
|
382
|
-
addEventListener(event: string, handler: (...args: unknown[]) => void) {
|
|
383
|
-
if (event === "open") wsOnOpen = handler as () => void;
|
|
384
|
-
if (event === "message")
|
|
385
|
-
wsOnMessage = handler as (event: { data: string }) => void;
|
|
386
|
-
}
|
|
387
|
-
send(data: string) {
|
|
388
|
-
sentMessages.push(data);
|
|
389
|
-
const msg = JSON.parse(data);
|
|
390
|
-
if (msg.method === "Browser.getWindowForTarget") {
|
|
391
|
-
setTimeout(() => {
|
|
392
|
-
wsOnMessage?.({
|
|
393
|
-
data: JSON.stringify({ id: 1, result: { windowId: 42 } }),
|
|
394
|
-
});
|
|
395
|
-
}, 0);
|
|
396
|
-
} else if (msg.method === "Browser.setWindowBounds") {
|
|
397
|
-
setTimeout(() => {
|
|
398
|
-
wsOnMessage?.({ data: JSON.stringify({ id: 2, result: {} }) });
|
|
399
|
-
}, 0);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
close() {}
|
|
403
|
-
} as unknown as typeof WebSocket;
|
|
404
|
-
|
|
405
|
-
const promise = restoreChromeWindow();
|
|
406
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
407
|
-
wsOnOpen?.();
|
|
408
|
-
await promise;
|
|
409
|
-
|
|
410
|
-
const boundsMsg = sentMessages.find((m) =>
|
|
411
|
-
m.includes("Browser.setWindowBounds"),
|
|
412
|
-
);
|
|
413
|
-
expect(boundsMsg).toBeDefined();
|
|
414
|
-
const parsed = JSON.parse(boundsMsg!);
|
|
415
|
-
expect(parsed.params.bounds.windowState).toBe("normal");
|
|
416
|
-
|
|
417
|
-
globalThis.WebSocket = OriginalWebSocket;
|
|
418
|
-
});
|
|
419
|
-
});
|