@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
|
@@ -9,37 +9,74 @@ mock.module("../util/logger.js", () => ({
|
|
|
9
9
|
}),
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Shared fake CDP session state. Tests install a custom `cdpSend`
|
|
14
|
+
* implementation in their setup, then assert against `cdpCalls` and
|
|
15
|
+
* `detachCalls` after the tool runs.
|
|
16
|
+
*
|
|
17
|
+
* Rather than mocking `factory.js` or `local-cdp-client.js` directly
|
|
18
|
+
* (both of which would leak module-level mocks into other test files
|
|
19
|
+
* via bun's shared mock registry), we only mock `browser-manager.js`
|
|
20
|
+
* and return a fake Playwright page whose CDP session routes through
|
|
21
|
+
* a programmable handler. That lets the real `LocalCdpClient` +
|
|
22
|
+
* `getCdpClient` factory code run end-to-end, so this file does not
|
|
23
|
+
* interfere with `local-cdp-client.test.ts` or `factory.test.ts`.
|
|
24
|
+
*/
|
|
25
|
+
type CdpCall = { method: string; params: Record<string, unknown> };
|
|
26
|
+
let cdpCalls: CdpCall[] = [];
|
|
27
|
+
let cdpSend: (
|
|
28
|
+
method: string,
|
|
29
|
+
params?: Record<string, unknown>,
|
|
30
|
+
) => Promise<unknown>;
|
|
31
|
+
let detachCalls: number;
|
|
20
32
|
|
|
21
33
|
let closeSessionPageMock: ReturnType<typeof mock>;
|
|
22
34
|
let closeAllPagesMock: ReturnType<typeof mock>;
|
|
23
|
-
let
|
|
24
|
-
let
|
|
35
|
+
let clearSnapshotBackendNodeMapMock: ReturnType<typeof mock>;
|
|
36
|
+
let storeSnapshotBackendNodeMapMock: ReturnType<typeof mock>;
|
|
37
|
+
let storedBackendNodeMaps: Map<string, Map<string, number>>;
|
|
25
38
|
|
|
26
39
|
mock.module("../tools/browser/browser-manager.js", () => {
|
|
27
|
-
|
|
40
|
+
storedBackendNodeMaps = new Map();
|
|
28
41
|
closeSessionPageMock = mock(async () => {});
|
|
29
42
|
closeAllPagesMock = mock(async () => {});
|
|
30
|
-
|
|
31
|
-
(conversationId
|
|
32
|
-
|
|
43
|
+
clearSnapshotBackendNodeMapMock = mock((conversationId: string) => {
|
|
44
|
+
storedBackendNodeMaps.delete(conversationId);
|
|
45
|
+
});
|
|
46
|
+
storeSnapshotBackendNodeMapMock = mock(
|
|
47
|
+
(conversationId: string, map: Map<string, number>) => {
|
|
48
|
+
storedBackendNodeMaps.set(conversationId, map);
|
|
33
49
|
},
|
|
34
50
|
);
|
|
51
|
+
// Fake Playwright page whose CDPSession routes to our per-test
|
|
52
|
+
// handler. LocalCdpClient lazily creates the session on first send,
|
|
53
|
+
// which is how the real tool path drives us.
|
|
54
|
+
const fakeSession = {
|
|
55
|
+
send: async (method: string, params?: Record<string, unknown>) => {
|
|
56
|
+
cdpCalls.push({ method, params: params ?? {} });
|
|
57
|
+
return cdpSend(method, params);
|
|
58
|
+
},
|
|
59
|
+
detach: async () => {
|
|
60
|
+
detachCalls += 1;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
const fakePage = {
|
|
64
|
+
context: () => ({
|
|
65
|
+
newCDPSession: async () => fakeSession,
|
|
66
|
+
}),
|
|
67
|
+
};
|
|
35
68
|
return {
|
|
36
69
|
browserManager: {
|
|
37
|
-
getOrCreateSessionPage: async () =>
|
|
70
|
+
getOrCreateSessionPage: async (_conversationId: string) => fakePage,
|
|
38
71
|
closeSessionPage: closeSessionPageMock,
|
|
39
72
|
closeAllPages: closeAllPagesMock,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
storeSnapshotBackendNodeMap: storeSnapshotBackendNodeMapMock,
|
|
74
|
+
clearSnapshotBackendNodeMap: clearSnapshotBackendNodeMapMock,
|
|
75
|
+
resolveSnapshotBackendNodeId: (
|
|
76
|
+
conversationId: string,
|
|
77
|
+
elementId: string,
|
|
78
|
+
) => {
|
|
79
|
+
const map = storedBackendNodeMaps.get(conversationId);
|
|
43
80
|
if (!map) return null;
|
|
44
81
|
return map.get(elementId) ?? null;
|
|
45
82
|
},
|
|
@@ -67,95 +104,221 @@ const ctx: ToolContext = {
|
|
|
67
104
|
trustClass: "guardian",
|
|
68
105
|
};
|
|
69
106
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
107
|
+
// ── Fixtures ─────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Minimal three-element Accessibility.getFullAXTree fixture. Mirrors
|
|
111
|
+
* the shape CDP returns — a flat array of nodes with parent/child
|
|
112
|
+
* references via `childIds`. Roles are chosen from the snapshot
|
|
113
|
+
* transformer's interactive-role allowlist so the output contains
|
|
114
|
+
* exactly three elements. The root `WebArea` node owns all three as
|
|
115
|
+
* children so document-order traversal yields e1/e2/e3.
|
|
116
|
+
*/
|
|
117
|
+
function buildAxTreeFixture(): { nodes: Record<string, unknown>[] } {
|
|
118
|
+
return {
|
|
119
|
+
nodes: [
|
|
120
|
+
{
|
|
121
|
+
nodeId: "1",
|
|
122
|
+
role: { type: "role", value: "WebArea" },
|
|
123
|
+
name: { type: "computedString", value: "" },
|
|
124
|
+
childIds: ["2", "3", "4"],
|
|
125
|
+
backendDOMNodeId: 1,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
nodeId: "2",
|
|
129
|
+
role: { type: "role", value: "link" },
|
|
130
|
+
name: { type: "computedString", value: "About Us" },
|
|
131
|
+
properties: [
|
|
132
|
+
{ name: "url", value: { type: "string", value: "/about" } },
|
|
133
|
+
],
|
|
134
|
+
childIds: [],
|
|
135
|
+
backendDOMNodeId: 42,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
nodeId: "3",
|
|
139
|
+
role: { type: "role", value: "button" },
|
|
140
|
+
name: { type: "computedString", value: "Submit" },
|
|
141
|
+
properties: [],
|
|
142
|
+
childIds: [],
|
|
143
|
+
backendDOMNodeId: 99,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
nodeId: "4",
|
|
147
|
+
role: { type: "role", value: "textbox" },
|
|
148
|
+
name: { type: "computedString", value: "Email" },
|
|
149
|
+
properties: [
|
|
150
|
+
{
|
|
151
|
+
name: "placeholder",
|
|
152
|
+
value: { type: "string", value: "you@example.com" },
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
childIds: [],
|
|
156
|
+
backendDOMNodeId: 101,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build a default CDP `send` handler that returns canned success
|
|
164
|
+
* responses for the methods `executeBrowserSnapshot` touches. Tests
|
|
165
|
+
* pass a map of overrides to customise individual responses or throw
|
|
166
|
+
* from a specific method.
|
|
167
|
+
*/
|
|
168
|
+
function installCdpSend(
|
|
169
|
+
overrides: Partial<{
|
|
170
|
+
url: string;
|
|
171
|
+
title: string;
|
|
172
|
+
axTree: unknown;
|
|
173
|
+
throwFrom: string;
|
|
174
|
+
}> = {},
|
|
175
|
+
) {
|
|
176
|
+
const url = overrides.url ?? "https://example.com/";
|
|
177
|
+
const title = overrides.title ?? "Example Page";
|
|
178
|
+
const axTree = overrides.axTree ?? { nodes: [] };
|
|
179
|
+
const throwFrom = overrides.throwFrom;
|
|
180
|
+
|
|
181
|
+
let runtimeEvaluateCall = 0;
|
|
182
|
+
cdpSend = async (method, _params) => {
|
|
183
|
+
if (throwFrom === method) {
|
|
184
|
+
throw new Error("tab detached");
|
|
185
|
+
}
|
|
186
|
+
switch (method) {
|
|
187
|
+
case "Runtime.evaluate": {
|
|
188
|
+
// First call → URL, second → title. Anything beyond returns "".
|
|
189
|
+
const value =
|
|
190
|
+
runtimeEvaluateCall === 0
|
|
191
|
+
? url
|
|
192
|
+
: runtimeEvaluateCall === 1
|
|
193
|
+
? title
|
|
194
|
+
: "";
|
|
195
|
+
runtimeEvaluateCall += 1;
|
|
196
|
+
return { result: { value } };
|
|
197
|
+
}
|
|
198
|
+
case "Accessibility.enable":
|
|
199
|
+
return {};
|
|
200
|
+
case "Accessibility.getFullAXTree":
|
|
201
|
+
return axTree;
|
|
202
|
+
default:
|
|
203
|
+
return {};
|
|
204
|
+
}
|
|
81
205
|
};
|
|
206
|
+
return { url, title };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function resetCdpState() {
|
|
210
|
+
cdpCalls = [];
|
|
211
|
+
detachCalls = 0;
|
|
212
|
+
cdpSend = async () => ({});
|
|
82
213
|
}
|
|
83
214
|
|
|
84
215
|
// ── browser_snapshot ─────────────────────────────────────────────────
|
|
85
216
|
|
|
86
|
-
describe("executeBrowserSnapshot", () => {
|
|
217
|
+
describe("executeBrowserSnapshot (CDP Accessibility.getFullAXTree)", () => {
|
|
87
218
|
beforeEach(() => {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
219
|
+
resetCdpState();
|
|
220
|
+
storedBackendNodeMaps.clear();
|
|
221
|
+
storeSnapshotBackendNodeMapMock.mockClear();
|
|
91
222
|
});
|
|
92
223
|
|
|
93
|
-
test("returns
|
|
224
|
+
test("returns URL, title, and a list of interactive elements", async () => {
|
|
225
|
+
installCdpSend({
|
|
226
|
+
url: "https://example.com/",
|
|
227
|
+
title: "Example Page",
|
|
228
|
+
axTree: buildAxTreeFixture(),
|
|
229
|
+
});
|
|
230
|
+
|
|
94
231
|
const result = await executeBrowserSnapshot({}, ctx);
|
|
232
|
+
|
|
95
233
|
expect(result.isError).toBe(false);
|
|
96
234
|
expect(result.content).toContain("URL: https://example.com/");
|
|
97
|
-
expect(result.content).toContain("Title:
|
|
98
|
-
expect(result.content).toContain("
|
|
235
|
+
expect(result.content).toContain("Title: Example Page");
|
|
236
|
+
expect(result.content).toContain("[e1] <link");
|
|
237
|
+
expect(result.content).toContain("About Us");
|
|
238
|
+
expect(result.content).toContain("[e2] <button> Submit");
|
|
239
|
+
expect(result.content).toContain("[e3] <textbox");
|
|
240
|
+
expect(result.content).toContain("Email");
|
|
241
|
+
expect(result.content).toContain("3 interactive elements found.");
|
|
99
242
|
});
|
|
100
243
|
|
|
101
|
-
test("
|
|
102
|
-
|
|
103
|
-
{ eid: "e1", tag: "a", attrs: { href: "/about" }, text: "About Us" },
|
|
104
|
-
{ eid: "e2", tag: "button", attrs: {}, text: "Submit" },
|
|
105
|
-
{
|
|
106
|
-
eid: "e3",
|
|
107
|
-
tag: "input",
|
|
108
|
-
attrs: { type: "text", name: "email", placeholder: "Email" },
|
|
109
|
-
text: "",
|
|
110
|
-
},
|
|
111
|
-
]);
|
|
244
|
+
test("calls Accessibility.enable and getFullAXTree via CDP", async () => {
|
|
245
|
+
installCdpSend();
|
|
112
246
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
expect(
|
|
117
|
-
expect(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
247
|
+
await executeBrowserSnapshot({}, ctx);
|
|
248
|
+
|
|
249
|
+
const methods = cdpCalls.map((c) => c.method);
|
|
250
|
+
expect(methods).toContain("Accessibility.enable");
|
|
251
|
+
expect(methods).toContain("Accessibility.getFullAXTree");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("stores backendNodeId map keyed by eid", async () => {
|
|
255
|
+
installCdpSend({
|
|
256
|
+
axTree: buildAxTreeFixture(),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await executeBrowserSnapshot({}, ctx);
|
|
260
|
+
|
|
261
|
+
expect(storeSnapshotBackendNodeMapMock).toHaveBeenCalledTimes(1);
|
|
262
|
+
const backendMap = storedBackendNodeMaps.get("test-conversation");
|
|
263
|
+
expect(backendMap).toBeDefined();
|
|
264
|
+
expect(backendMap!.get("e1")).toBe(42);
|
|
265
|
+
expect(backendMap!.get("e2")).toBe(99);
|
|
266
|
+
expect(backendMap!.get("e3")).toBe(101);
|
|
121
267
|
});
|
|
122
268
|
|
|
123
|
-
test("
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
]);
|
|
269
|
+
test("does not invoke the legacy DOM tagging bridge", async () => {
|
|
270
|
+
installCdpSend({
|
|
271
|
+
axTree: buildAxTreeFixture(),
|
|
272
|
+
});
|
|
128
273
|
|
|
129
274
|
await executeBrowserSnapshot({}, ctx);
|
|
130
|
-
expect(storeSnapshotMapMock).toHaveBeenCalledTimes(1);
|
|
131
275
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
expect(
|
|
276
|
+
// The legacy eid → data-vellum-eid bridge has been removed;
|
|
277
|
+
// interaction tools use the backendNodeId map directly.
|
|
278
|
+
const methods = cdpCalls.map((c) => c.method);
|
|
279
|
+
expect(methods).not.toContain("DOM.pushNodesByBackendIdsToFrontend");
|
|
280
|
+
expect(methods).not.toContain("DOM.setAttributeValue");
|
|
136
281
|
});
|
|
137
282
|
|
|
138
|
-
test("
|
|
139
|
-
|
|
140
|
-
{ eid: "e1", tag: "button", attrs: {}, text: "Click" },
|
|
141
|
-
]);
|
|
283
|
+
test("renders '(no interactive elements found)' on empty AX tree", async () => {
|
|
284
|
+
installCdpSend({ axTree: { nodes: [] } });
|
|
142
285
|
|
|
143
286
|
const result = await executeBrowserSnapshot({}, ctx);
|
|
144
|
-
expect(result.
|
|
287
|
+
expect(result.isError).toBe(false);
|
|
288
|
+
expect(result.content).toContain("(no interactive elements found)");
|
|
289
|
+
// Empty map should still be stored so stale eids from a previous
|
|
290
|
+
// snapshot cannot resolve after this call.
|
|
291
|
+
expect(storeSnapshotBackendNodeMapMock).toHaveBeenCalledTimes(1);
|
|
292
|
+
const backendMap = storedBackendNodeMaps.get("test-conversation");
|
|
293
|
+
expect(backendMap?.size ?? 0).toBe(0);
|
|
145
294
|
});
|
|
146
295
|
|
|
147
|
-
test("
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
});
|
|
296
|
+
test("returns error content when Accessibility.getFullAXTree throws", async () => {
|
|
297
|
+
installCdpSend({ throwFrom: "Accessibility.getFullAXTree" });
|
|
298
|
+
|
|
151
299
|
const result = await executeBrowserSnapshot({}, ctx);
|
|
300
|
+
|
|
152
301
|
expect(result.isError).toBe(true);
|
|
153
302
|
expect(result.content).toContain("Snapshot failed");
|
|
154
|
-
expect(result.content).toContain("
|
|
303
|
+
expect(result.content).toContain("tab detached");
|
|
304
|
+
// dispose still runs in the finally block, which schedules an
|
|
305
|
+
// async session.detach() — flush microtasks before asserting.
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
307
|
+
expect(detachCalls).toBe(1);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("disposes the CdpClient even on success", async () => {
|
|
311
|
+
installCdpSend();
|
|
312
|
+
|
|
313
|
+
await executeBrowserSnapshot({}, ctx);
|
|
314
|
+
// dispose schedules detach asynchronously; flush microtasks.
|
|
315
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
316
|
+
|
|
317
|
+
expect(detachCalls).toBe(1);
|
|
155
318
|
});
|
|
156
319
|
|
|
157
320
|
test("shows (none) for empty title", async () => {
|
|
158
|
-
|
|
321
|
+
installCdpSend({ title: "", axTree: { nodes: [] } });
|
|
159
322
|
const result = await executeBrowserSnapshot({}, ctx);
|
|
160
323
|
expect(result.content).toContain("Title: (none)");
|
|
161
324
|
});
|
|
@@ -165,7 +328,7 @@ describe("executeBrowserSnapshot", () => {
|
|
|
165
328
|
|
|
166
329
|
describe("executeBrowserClose", () => {
|
|
167
330
|
beforeEach(() => {
|
|
168
|
-
|
|
331
|
+
resetCdpState();
|
|
169
332
|
closeSessionPageMock.mockClear();
|
|
170
333
|
closeAllPagesMock.mockClear();
|
|
171
334
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect, mock, test } from "bun:test";
|
|
1
|
+
import { afterEach, describe, expect, jest, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
const mockConfig = {
|
|
4
4
|
timeouts: {
|
|
@@ -393,6 +393,155 @@ describe("HostBashProxy", () => {
|
|
|
393
393
|
});
|
|
394
394
|
});
|
|
395
395
|
|
|
396
|
+
describe("abort listener lifecycle", () => {
|
|
397
|
+
// Helper that wraps an AbortSignal to observe add/removeEventListener
|
|
398
|
+
// invocations without tripping over tsc's strict overload matching on
|
|
399
|
+
// AbortSignal itself.
|
|
400
|
+
type Spied = {
|
|
401
|
+
signal: AbortSignal;
|
|
402
|
+
addCalls: string[];
|
|
403
|
+
removeCalls: string[];
|
|
404
|
+
};
|
|
405
|
+
function spySignal(source: AbortSignal): Spied {
|
|
406
|
+
const addCalls: string[] = [];
|
|
407
|
+
const removeCalls: string[] = [];
|
|
408
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
409
|
+
const s = source as any;
|
|
410
|
+
const origAdd = source.addEventListener.bind(source);
|
|
411
|
+
const origRemove = source.removeEventListener.bind(source);
|
|
412
|
+
s.addEventListener = (
|
|
413
|
+
type: string,
|
|
414
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
415
|
+
...rest: any[]
|
|
416
|
+
) => {
|
|
417
|
+
addCalls.push(type);
|
|
418
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
419
|
+
return (origAdd as any)(type, ...rest);
|
|
420
|
+
};
|
|
421
|
+
s.removeEventListener = (
|
|
422
|
+
type: string,
|
|
423
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
424
|
+
...rest: any[]
|
|
425
|
+
) => {
|
|
426
|
+
removeCalls.push(type);
|
|
427
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
428
|
+
return (origRemove as any)(type, ...rest);
|
|
429
|
+
};
|
|
430
|
+
return { signal: source, addCalls, removeCalls };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
test("removes abort listener from signal after resolve completes", async () => {
|
|
434
|
+
setup();
|
|
435
|
+
const controller = new AbortController();
|
|
436
|
+
const spy = spySignal(controller.signal);
|
|
437
|
+
|
|
438
|
+
const resultPromise = proxy.request(
|
|
439
|
+
{ command: "echo hello" },
|
|
440
|
+
"session-1",
|
|
441
|
+
spy.signal,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
expect(spy.addCalls).toEqual(["abort"]);
|
|
445
|
+
expect(spy.removeCalls).toEqual([]);
|
|
446
|
+
|
|
447
|
+
const requestId = (sentMessages[0] as Record<string, unknown>)
|
|
448
|
+
.requestId as string;
|
|
449
|
+
proxy.resolve(requestId, {
|
|
450
|
+
stdout: "hello\n",
|
|
451
|
+
stderr: "",
|
|
452
|
+
exitCode: 0,
|
|
453
|
+
timedOut: false,
|
|
454
|
+
});
|
|
455
|
+
await resultPromise;
|
|
456
|
+
|
|
457
|
+
// Listener is detached after normal completion.
|
|
458
|
+
expect(spy.removeCalls).toEqual(["abort"]);
|
|
459
|
+
|
|
460
|
+
// Subsequent aborts are harmless no-ops (no side effects on the proxy).
|
|
461
|
+
controller.abort();
|
|
462
|
+
// No additional emitted envelopes from the late abort.
|
|
463
|
+
expect(sentMessages).toHaveLength(1);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("removes abort listener from signal on timer timeout", async () => {
|
|
467
|
+
setup();
|
|
468
|
+
|
|
469
|
+
jest.useFakeTimers();
|
|
470
|
+
try {
|
|
471
|
+
const controller = new AbortController();
|
|
472
|
+
const spy = spySignal(controller.signal);
|
|
473
|
+
|
|
474
|
+
const resultPromise = proxy.request(
|
|
475
|
+
{ command: "echo slow", timeout_seconds: 30 },
|
|
476
|
+
"session-1",
|
|
477
|
+
spy.signal,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
expect(spy.addCalls).toEqual(["abort"]);
|
|
481
|
+
expect(spy.removeCalls).toEqual([]);
|
|
482
|
+
|
|
483
|
+
// Proxy timeout is timeout_seconds + 3 = 33s. Advance past it.
|
|
484
|
+
jest.advanceTimersByTime(34 * 1000);
|
|
485
|
+
|
|
486
|
+
const result = await resultPromise;
|
|
487
|
+
expect(result.isError).toBe(true);
|
|
488
|
+
expect(result.content).toContain("Host bash proxy timed out");
|
|
489
|
+
|
|
490
|
+
// Listener is detached after the timer fires.
|
|
491
|
+
expect(spy.removeCalls).toEqual(["abort"]);
|
|
492
|
+
|
|
493
|
+
// Subsequent aborts should be harmless — no cancel emitted.
|
|
494
|
+
controller.abort();
|
|
495
|
+
expect(sentMessages).toHaveLength(1);
|
|
496
|
+
} finally {
|
|
497
|
+
jest.useRealTimers();
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
describe("sender throws synchronously", () => {
|
|
503
|
+
test("rejects the promise, clears pending state and timer, invokes onInternalResolve", async () => {
|
|
504
|
+
const resolvedIds: string[] = [];
|
|
505
|
+
sentMessages = [];
|
|
506
|
+
sendToClient = () => {
|
|
507
|
+
throw new Error("transport down");
|
|
508
|
+
};
|
|
509
|
+
proxy = new HostBashProxy(sendToClient, (id) => resolvedIds.push(id));
|
|
510
|
+
|
|
511
|
+
// request() synchronously calls sendToClient inside the Promise
|
|
512
|
+
// executor. A throw there surfaces as a rejected promise.
|
|
513
|
+
const resultPromise = proxy.request(
|
|
514
|
+
{ command: "echo hello" },
|
|
515
|
+
"session-1",
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
await expect(resultPromise).rejects.toThrow("transport down");
|
|
519
|
+
|
|
520
|
+
// The internal resolve should fire exactly once as part of cleanup.
|
|
521
|
+
expect(resolvedIds).toHaveLength(1);
|
|
522
|
+
|
|
523
|
+
// Issue a new request on a fresh (non-throwing) sender and verify
|
|
524
|
+
// the proxy is still functional — no stale timers or bookkeeping
|
|
525
|
+
// from the failed request.
|
|
526
|
+
sentMessages = [];
|
|
527
|
+
proxy.updateSender((msg) => sentMessages.push(msg), true);
|
|
528
|
+
const okPromise = proxy.request({ command: "echo ok" }, "session-1");
|
|
529
|
+
expect(sentMessages).toHaveLength(1);
|
|
530
|
+
const okRequestId = (sentMessages[0] as Record<string, unknown>)
|
|
531
|
+
.requestId as string;
|
|
532
|
+
expect(proxy.hasPendingRequest(okRequestId)).toBe(true);
|
|
533
|
+
proxy.resolve(okRequestId, {
|
|
534
|
+
stdout: "ok\n",
|
|
535
|
+
stderr: "",
|
|
536
|
+
exitCode: 0,
|
|
537
|
+
timedOut: false,
|
|
538
|
+
});
|
|
539
|
+
const okResult = await okPromise;
|
|
540
|
+
expect(okResult.content).toContain("ok");
|
|
541
|
+
expect(okResult.isError).toBe(false);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
396
545
|
describe("onInternalResolve callback", () => {
|
|
397
546
|
test("fires on abort", async () => {
|
|
398
547
|
const resolvedIds: string[] = [];
|