@vellumai/assistant 0.6.1 → 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/docker-entrypoint.sh +12 -2
- package/docs/architecture/memory.md +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- 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__/assistant-event-hub.test.ts +30 -0
- 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__/checker.test.ts +104 -170
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- 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 +21 -6
- 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 +169 -0
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -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 -3
- 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__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- package/src/__tests__/log-export-workspace.test.ts +190 -0
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- 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__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -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 +74 -55
- 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__/pkb-autoinject.test.ts +96 -0
- 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 -3
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- 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-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +11 -5
- package/src/__tests__/test-preload.ts +14 -0
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +0 -1
- 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 +62 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- package/src/__tests__/v2-consent-policy.test.ts +103 -0
- package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/acp/client-handler.ts +30 -4
- package/src/agent/loop.ts +12 -35
- 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 +55 -0
- package/src/cli/__tests__/run-assistant-command.ts +34 -7
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/browser-relay.ts +339 -409
- package/src/cli/commands/credentials.ts +3 -3
- package/src/cli/commands/default-action.ts +68 -1
- 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 +68 -41
- 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 +16 -2
- 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/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- 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 +10 -3
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
- 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 +12 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- package/src/config/bundled-skills/outlook/SKILL.md +7 -0
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- 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 +46 -7
- 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 +16 -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 -16
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/config-watcher.ts +6 -2
- package/src/daemon/context-overflow-approval.ts +5 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
- package/src/daemon/conversation-agent-loop.ts +74 -19
- package/src/daemon/conversation-attachments.ts +40 -1
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +66 -3
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +159 -20
- package/src/daemon/conversation-surfaces.ts +78 -12
- package/src/daemon/conversation-tool-setup.ts +74 -11
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +227 -11
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/conversations.ts +9 -139
- package/src/daemon/handlers/shared.ts +65 -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 +86 -12
- package/src/daemon/message-protocol.ts +7 -0
- package/src/daemon/message-types/conversations.ts +59 -13
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/messages.ts +5 -6
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/settings.ts +12 -0
- package/src/daemon/message-types/skills.ts +10 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/server.ts +112 -35
- package/src/daemon/tool-side-effects.ts +6 -0
- package/src/daemon/transport-hints.ts +14 -0
- package/src/inbound/platform-callback-registration.ts +18 -17
- package/src/index.ts +1 -1
- package/src/mcp/client.ts +59 -24
- package/src/memory/app-store.ts +31 -1
- package/src/memory/conversation-crud.ts +38 -10
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +65 -5
- 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 +177 -18
- package/src/memory/graph/capability-seed.ts +3 -5
- 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/group-crud.ts +25 -9
- 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/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- 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 +5 -5
- 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 +127 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -3
- package/src/permissions/v2-consent-policy.ts +87 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -21
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -96
- package/src/prompts/templates/SOUL.md +11 -11
- 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 +24 -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/auth/token-service.ts +8 -0
- 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 +18 -5
- package/src/runtime/routes/conversation-management-routes.ts +108 -0
- package/src/runtime/routes/conversation-routes.ts +308 -28
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/group-routes.ts +22 -8
- 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/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +60 -25
- 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/skills/inline-command-runner.ts +12 -14
- 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 -100
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -1
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- package/src/tools/skills/sandbox-runner.ts +3 -6
- 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/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +24 -21
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/types.ts +2 -3
- package/src/util/platform.ts +14 -19
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/029-seed-pkb.ts +1 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/workspace/top-level-renderer.ts +19 -1
- package/src/__tests__/chrome-cdp.test.ts +0 -419
- package/src/__tests__/permission-mode-sse.test.ts +0 -418
- package/src/__tests__/permission-mode-store.test.ts +0 -277
- package/src/browser-extension-relay/protocol.ts +0 -63
- package/src/browser-extension-relay/server.ts +0 -203
- package/src/config/schemas/sandbox.ts +0 -14
- package/src/permissions/permission-mode-store.ts +0 -180
- package/src/tools/browser/chrome-cdp.ts +0 -239
- package/src/tools/system/set-permission-mode.ts +0 -103
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const { HostBrowserProxy } = await import("../daemon/host-browser-proxy.js");
|
|
4
|
+
|
|
5
|
+
describe("HostBrowserProxy", () => {
|
|
6
|
+
let proxy: InstanceType<typeof HostBrowserProxy>;
|
|
7
|
+
let sentMessages: unknown[];
|
|
8
|
+
let sendToClient: (msg: unknown) => void;
|
|
9
|
+
|
|
10
|
+
function setup(onInternalResolve?: (requestId: string) => void) {
|
|
11
|
+
sentMessages = [];
|
|
12
|
+
sendToClient = (msg: unknown) => sentMessages.push(msg);
|
|
13
|
+
proxy = new HostBrowserProxy(sendToClient, onInternalResolve);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
proxy?.dispose();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("request/resolve lifecycle (happy path)", () => {
|
|
21
|
+
test("sends host_browser_request and resolves with content", async () => {
|
|
22
|
+
setup();
|
|
23
|
+
|
|
24
|
+
const resultPromise = proxy.request(
|
|
25
|
+
{
|
|
26
|
+
cdpMethod: "Page.navigate",
|
|
27
|
+
cdpParams: { url: "https://example.com" },
|
|
28
|
+
},
|
|
29
|
+
"session-1",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Verify the request was sent to the client
|
|
33
|
+
expect(sentMessages).toHaveLength(1);
|
|
34
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
35
|
+
expect(sent.type).toBe("host_browser_request");
|
|
36
|
+
expect(sent.conversationId).toBe("session-1");
|
|
37
|
+
expect(sent.cdpMethod).toBe("Page.navigate");
|
|
38
|
+
expect(sent.cdpParams).toEqual({ url: "https://example.com" });
|
|
39
|
+
expect(typeof sent.requestId).toBe("string");
|
|
40
|
+
|
|
41
|
+
const requestId = sent.requestId as string;
|
|
42
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(true);
|
|
43
|
+
|
|
44
|
+
// Simulate client response
|
|
45
|
+
proxy.resolve(requestId, {
|
|
46
|
+
content: "ok",
|
|
47
|
+
isError: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const result = await resultPromise;
|
|
51
|
+
expect(result.content).toBe("ok");
|
|
52
|
+
expect(result.isError).toBe(false);
|
|
53
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("forwards cdpParams and cdpSessionId on the emitted envelope", async () => {
|
|
57
|
+
setup();
|
|
58
|
+
|
|
59
|
+
const resultPromise = proxy.request(
|
|
60
|
+
{
|
|
61
|
+
cdpMethod: "Runtime.evaluate",
|
|
62
|
+
cdpParams: { expression: "document.title", returnByValue: true },
|
|
63
|
+
cdpSessionId: "session-abc",
|
|
64
|
+
},
|
|
65
|
+
"session-1",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(sentMessages).toHaveLength(1);
|
|
69
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
70
|
+
expect(sent.type).toBe("host_browser_request");
|
|
71
|
+
expect(sent.cdpMethod).toBe("Runtime.evaluate");
|
|
72
|
+
expect(sent.cdpParams).toEqual({
|
|
73
|
+
expression: "document.title",
|
|
74
|
+
returnByValue: true,
|
|
75
|
+
});
|
|
76
|
+
expect(sent.cdpSessionId).toBe("session-abc");
|
|
77
|
+
|
|
78
|
+
const requestId = sent.requestId as string;
|
|
79
|
+
proxy.resolve(requestId, {
|
|
80
|
+
content: "Example Domain",
|
|
81
|
+
isError: false,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await resultPromise;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("resolves error responses correctly", async () => {
|
|
88
|
+
setup();
|
|
89
|
+
|
|
90
|
+
const resultPromise = proxy.request(
|
|
91
|
+
{ cdpMethod: "Page.navigate", cdpParams: { url: "invalid://" } },
|
|
92
|
+
"session-1",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
96
|
+
const requestId = sent.requestId as string;
|
|
97
|
+
|
|
98
|
+
proxy.resolve(requestId, {
|
|
99
|
+
content: "Navigation failed",
|
|
100
|
+
isError: true,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await resultPromise;
|
|
104
|
+
expect(result.isError).toBe(true);
|
|
105
|
+
expect(result.content).toContain("Navigation failed");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("pending tracking", () => {
|
|
110
|
+
test("hasPendingRequest returns true after request and false after resolve", async () => {
|
|
111
|
+
setup();
|
|
112
|
+
|
|
113
|
+
const resultPromise = proxy.request(
|
|
114
|
+
{ cdpMethod: "Page.navigate", cdpParams: { url: "https://a.test" } },
|
|
115
|
+
"session-1",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
119
|
+
const requestId = sent.requestId as string;
|
|
120
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(true);
|
|
121
|
+
|
|
122
|
+
proxy.resolve(requestId, { content: "ok", isError: false });
|
|
123
|
+
|
|
124
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(false);
|
|
125
|
+
await resultPromise;
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("timeout", () => {
|
|
130
|
+
test("resolves with timeout error when proxy timeout fires", async () => {
|
|
131
|
+
const resolvedIds: string[] = [];
|
|
132
|
+
setup((id) => resolvedIds.push(id));
|
|
133
|
+
|
|
134
|
+
const resultPromise = proxy.request(
|
|
135
|
+
{
|
|
136
|
+
cdpMethod: "Page.navigate",
|
|
137
|
+
cdpParams: { url: "https://slow.test" },
|
|
138
|
+
// Sub-second timeout to trigger the timer quickly.
|
|
139
|
+
timeout_seconds: 0.01,
|
|
140
|
+
},
|
|
141
|
+
"session-1",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
145
|
+
const requestId = sent.requestId as string;
|
|
146
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(true);
|
|
147
|
+
|
|
148
|
+
// Wait long enough for the timer (10ms) to fire.
|
|
149
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
150
|
+
|
|
151
|
+
const result = await resultPromise;
|
|
152
|
+
expect(result.isError).toBe(true);
|
|
153
|
+
expect(result.content).toContain("Host browser proxy timed out");
|
|
154
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(false);
|
|
155
|
+
expect(resolvedIds).toEqual([requestId]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("abort signal", () => {
|
|
160
|
+
test("returns immediately if signal already aborted", async () => {
|
|
161
|
+
setup();
|
|
162
|
+
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
controller.abort();
|
|
165
|
+
|
|
166
|
+
const result = await proxy.request(
|
|
167
|
+
{ cdpMethod: "Page.navigate", cdpParams: { url: "https://a.test" } },
|
|
168
|
+
"session-1",
|
|
169
|
+
controller.signal,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(result.content).toBe("Aborted");
|
|
173
|
+
expect(result.isError).toBe(true);
|
|
174
|
+
expect(sentMessages).toHaveLength(0); // No envelope emitted.
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("mid-flight abort resolves with Aborted and emits host_browser_cancel", async () => {
|
|
178
|
+
const resolvedIds: string[] = [];
|
|
179
|
+
setup((id) => resolvedIds.push(id));
|
|
180
|
+
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const resultPromise = proxy.request(
|
|
183
|
+
{ cdpMethod: "Page.navigate", cdpParams: { url: "https://a.test" } },
|
|
184
|
+
"session-1",
|
|
185
|
+
controller.signal,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
189
|
+
const requestId = sent.requestId as string;
|
|
190
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(true);
|
|
191
|
+
|
|
192
|
+
controller.abort();
|
|
193
|
+
|
|
194
|
+
const result = await resultPromise;
|
|
195
|
+
expect(result.content).toBe("Aborted");
|
|
196
|
+
expect(result.isError).toBe(true);
|
|
197
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(false);
|
|
198
|
+
|
|
199
|
+
// Second message should be the cancel envelope.
|
|
200
|
+
expect(sentMessages).toHaveLength(2);
|
|
201
|
+
const cancelMsg = sentMessages[1] as Record<string, unknown>;
|
|
202
|
+
expect(cancelMsg.type).toBe("host_browser_cancel");
|
|
203
|
+
expect(cancelMsg.requestId).toBe(requestId);
|
|
204
|
+
|
|
205
|
+
// onInternalResolve should have been invoked.
|
|
206
|
+
expect(resolvedIds).toEqual([requestId]);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("isAvailable", () => {
|
|
211
|
+
test("returns false by default (no client connected)", () => {
|
|
212
|
+
setup();
|
|
213
|
+
expect(proxy.isAvailable()).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("returns true after updateSender with clientConnected=true", () => {
|
|
217
|
+
setup();
|
|
218
|
+
proxy.updateSender(sendToClient, true);
|
|
219
|
+
expect(proxy.isAvailable()).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("returns false after updateSender with clientConnected=false", () => {
|
|
223
|
+
setup();
|
|
224
|
+
proxy.updateSender(sendToClient, true);
|
|
225
|
+
expect(proxy.isAvailable()).toBe(true);
|
|
226
|
+
proxy.updateSender(sendToClient, false);
|
|
227
|
+
expect(proxy.isAvailable()).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("updateSender", () => {
|
|
232
|
+
test("uses updated sender for new requests", async () => {
|
|
233
|
+
setup();
|
|
234
|
+
|
|
235
|
+
const newMessages: unknown[] = [];
|
|
236
|
+
proxy.updateSender((msg) => newMessages.push(msg), true);
|
|
237
|
+
|
|
238
|
+
const resultPromise = proxy.request(
|
|
239
|
+
{ cdpMethod: "Page.navigate", cdpParams: { url: "https://a.test" } },
|
|
240
|
+
"session-1",
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(sentMessages).toHaveLength(0); // Old sender not used.
|
|
244
|
+
expect(newMessages).toHaveLength(1); // New sender used.
|
|
245
|
+
|
|
246
|
+
const sent = newMessages[0] as Record<string, unknown>;
|
|
247
|
+
proxy.resolve(sent.requestId as string, {
|
|
248
|
+
content: "ok",
|
|
249
|
+
isError: false,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await resultPromise;
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe("dispose", () => {
|
|
257
|
+
test("rejects all pending requests, emits cancels, invokes onInternalResolve", async () => {
|
|
258
|
+
const resolvedIds: string[] = [];
|
|
259
|
+
setup((id) => resolvedIds.push(id));
|
|
260
|
+
|
|
261
|
+
const p1 = proxy.request(
|
|
262
|
+
{ cdpMethod: "Page.navigate", cdpParams: { url: "https://a.test" } },
|
|
263
|
+
"session-1",
|
|
264
|
+
);
|
|
265
|
+
const p2 = proxy.request(
|
|
266
|
+
{ cdpMethod: "Page.navigate", cdpParams: { url: "https://b.test" } },
|
|
267
|
+
"session-1",
|
|
268
|
+
);
|
|
269
|
+
// Attach rejection handlers immediately so Bun doesn't flag the
|
|
270
|
+
// promises as unhandled before the awaited assertions run.
|
|
271
|
+
const p1Swallowed = p1.catch(() => {});
|
|
272
|
+
const p2Swallowed = p2.catch(() => {});
|
|
273
|
+
|
|
274
|
+
const requestIds = (sentMessages as Array<Record<string, unknown>>).map(
|
|
275
|
+
(m) => m.requestId as string,
|
|
276
|
+
);
|
|
277
|
+
expect(requestIds).toHaveLength(2);
|
|
278
|
+
expect(proxy.hasPendingRequest(requestIds[0]!)).toBe(true);
|
|
279
|
+
expect(proxy.hasPendingRequest(requestIds[1]!)).toBe(true);
|
|
280
|
+
|
|
281
|
+
proxy.dispose();
|
|
282
|
+
|
|
283
|
+
// Both pending requests should no longer be tracked.
|
|
284
|
+
expect(proxy.hasPendingRequest(requestIds[0]!)).toBe(false);
|
|
285
|
+
expect(proxy.hasPendingRequest(requestIds[1]!)).toBe(false);
|
|
286
|
+
|
|
287
|
+
// Both promises should reject with AssistantError message.
|
|
288
|
+
await expect(p1).rejects.toThrow("Host browser proxy disposed");
|
|
289
|
+
await expect(p2).rejects.toThrow("Host browser proxy disposed");
|
|
290
|
+
// Drain the swallowed copies so the unhandled-rejection guard clears.
|
|
291
|
+
await p1Swallowed;
|
|
292
|
+
await p2Swallowed;
|
|
293
|
+
|
|
294
|
+
// After the 2 request messages, dispose should have sent 2 cancel messages.
|
|
295
|
+
const cancelMessages = sentMessages
|
|
296
|
+
.slice(2)
|
|
297
|
+
.filter(
|
|
298
|
+
(m) => (m as Record<string, unknown>).type === "host_browser_cancel",
|
|
299
|
+
) as Array<Record<string, unknown>>;
|
|
300
|
+
expect(cancelMessages).toHaveLength(2);
|
|
301
|
+
expect(cancelMessages.map((m) => m.requestId)).toContain(requestIds[0]);
|
|
302
|
+
expect(cancelMessages.map((m) => m.requestId)).toContain(requestIds[1]);
|
|
303
|
+
|
|
304
|
+
// onInternalResolve fired for each pending request on dispose.
|
|
305
|
+
expect(resolvedIds).toHaveLength(2);
|
|
306
|
+
expect(resolvedIds).toContain(requestIds[0]!);
|
|
307
|
+
expect(resolvedIds).toContain(requestIds[1]!);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
describe("resolve with unknown requestId", () => {
|
|
312
|
+
test("silently ignores unknown requestId", () => {
|
|
313
|
+
setup();
|
|
314
|
+
// Should not throw.
|
|
315
|
+
proxy.resolve("nonexistent", {
|
|
316
|
+
content: "stale",
|
|
317
|
+
isError: false,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe("abort listener lifecycle", () => {
|
|
323
|
+
// Helper that wraps an AbortSignal to observe add/removeEventListener
|
|
324
|
+
// invocations without tripping over tsc's strict overload matching on
|
|
325
|
+
// AbortSignal itself.
|
|
326
|
+
type Spied = {
|
|
327
|
+
signal: AbortSignal;
|
|
328
|
+
addCalls: string[];
|
|
329
|
+
removeCalls: string[];
|
|
330
|
+
};
|
|
331
|
+
function spySignal(source: AbortSignal): Spied {
|
|
332
|
+
const addCalls: string[] = [];
|
|
333
|
+
const removeCalls: string[] = [];
|
|
334
|
+
const s = source as any;
|
|
335
|
+
const origAdd = source.addEventListener.bind(source);
|
|
336
|
+
const origRemove = source.removeEventListener.bind(source);
|
|
337
|
+
s.addEventListener = (
|
|
338
|
+
type: string,
|
|
339
|
+
...rest: any[]
|
|
340
|
+
) => {
|
|
341
|
+
addCalls.push(type);
|
|
342
|
+
return (origAdd as any)(type, ...rest);
|
|
343
|
+
};
|
|
344
|
+
s.removeEventListener = (
|
|
345
|
+
type: string,
|
|
346
|
+
...rest: any[]
|
|
347
|
+
) => {
|
|
348
|
+
removeCalls.push(type);
|
|
349
|
+
return (origRemove as any)(type, ...rest);
|
|
350
|
+
};
|
|
351
|
+
return { signal: source, addCalls, removeCalls };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
test("removes abort listener from signal after resolve completes", async () => {
|
|
355
|
+
setup();
|
|
356
|
+
const controller = new AbortController();
|
|
357
|
+
const spy = spySignal(controller.signal);
|
|
358
|
+
|
|
359
|
+
const resultPromise = proxy.request(
|
|
360
|
+
{ cdpMethod: "Page.reload" },
|
|
361
|
+
"session-1",
|
|
362
|
+
spy.signal,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
expect(spy.addCalls).toEqual(["abort"]);
|
|
366
|
+
expect(spy.removeCalls).toEqual([]);
|
|
367
|
+
|
|
368
|
+
const requestId = (sentMessages[0] as Record<string, unknown>)
|
|
369
|
+
.requestId as string;
|
|
370
|
+
proxy.resolve(requestId, { content: "ok", isError: false });
|
|
371
|
+
await resultPromise;
|
|
372
|
+
|
|
373
|
+
// Listener is detached after normal completion.
|
|
374
|
+
expect(spy.removeCalls).toEqual(["abort"]);
|
|
375
|
+
|
|
376
|
+
// Subsequent aborts are harmless no-ops (no side effects on the proxy).
|
|
377
|
+
controller.abort();
|
|
378
|
+
// No additional emitted envelopes from the late abort.
|
|
379
|
+
expect(sentMessages).toHaveLength(1);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("removes abort listener from signal after dispose", () => {
|
|
383
|
+
setup();
|
|
384
|
+
const controller = new AbortController();
|
|
385
|
+
const spy = spySignal(controller.signal);
|
|
386
|
+
|
|
387
|
+
const p = proxy.request(
|
|
388
|
+
{ cdpMethod: "Page.reload" },
|
|
389
|
+
"session-1",
|
|
390
|
+
spy.signal,
|
|
391
|
+
);
|
|
392
|
+
p.catch(() => {}); // Swallow expected rejection.
|
|
393
|
+
|
|
394
|
+
proxy.dispose();
|
|
395
|
+
|
|
396
|
+
expect(spy.removeCalls).toEqual(["abort"]);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("sender throws synchronously", () => {
|
|
401
|
+
test("rejects the promise, clears pending state and timer, invokes onInternalResolve", async () => {
|
|
402
|
+
const resolvedIds: string[] = [];
|
|
403
|
+
sentMessages = [];
|
|
404
|
+
sendToClient = () => {
|
|
405
|
+
throw new Error("transport down");
|
|
406
|
+
};
|
|
407
|
+
proxy = new HostBrowserProxy(sendToClient, (id) => resolvedIds.push(id));
|
|
408
|
+
|
|
409
|
+
// request() synchronously calls sendToClient inside the Promise
|
|
410
|
+
// executor. A throw there surfaces as a rejected promise.
|
|
411
|
+
const resultPromise = proxy.request(
|
|
412
|
+
{ cdpMethod: "Page.navigate", cdpParams: { url: "https://x.test" } },
|
|
413
|
+
"session-1",
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
await expect(resultPromise).rejects.toThrow("transport down");
|
|
417
|
+
|
|
418
|
+
// No entries should have leaked into the pending map.
|
|
419
|
+
// (We can't assert against a specific requestId because the sender
|
|
420
|
+
// threw before any message was observed, so there's nothing to read
|
|
421
|
+
// the id from. We can instead assert the internal resolve fired once
|
|
422
|
+
// and that no pending entries remain for any id we issue next.)
|
|
423
|
+
expect(resolvedIds).toHaveLength(1);
|
|
424
|
+
|
|
425
|
+
// Issue a new request on a fresh (non-throwing) sender and verify
|
|
426
|
+
// the proxy is still functional — no stale timers or bookkeeping
|
|
427
|
+
// from the failed request.
|
|
428
|
+
sentMessages = [];
|
|
429
|
+
proxy.updateSender((msg) => sentMessages.push(msg), true);
|
|
430
|
+
const okPromise = proxy.request(
|
|
431
|
+
{ cdpMethod: "Page.reload" },
|
|
432
|
+
"session-1",
|
|
433
|
+
);
|
|
434
|
+
expect(sentMessages).toHaveLength(1);
|
|
435
|
+
const okRequestId = (sentMessages[0] as Record<string, unknown>)
|
|
436
|
+
.requestId as string;
|
|
437
|
+
expect(proxy.hasPendingRequest(okRequestId)).toBe(true);
|
|
438
|
+
proxy.resolve(okRequestId, { content: "reloaded", isError: false });
|
|
439
|
+
const okResult = await okPromise;
|
|
440
|
+
expect(okResult.content).toBe("reloaded");
|
|
441
|
+
expect(okResult.isError).toBe(false);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the /v1/host-browser-result route handler.
|
|
3
|
+
*
|
|
4
|
+
* Tests handleHostBrowserResult directly with mocked AuthContext, Request,
|
|
5
|
+
* and a stub Conversation whose resolveHostBrowser method records calls.
|
|
6
|
+
*/
|
|
7
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
8
|
+
|
|
9
|
+
import type { Conversation } from "../daemon/conversation.js";
|
|
10
|
+
import type { AuthContext } from "../runtime/auth/types.js";
|
|
11
|
+
|
|
12
|
+
// ── Module mocks ─────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
let fakeHttpAuthDisabled = true;
|
|
15
|
+
|
|
16
|
+
mock.module("../config/env.js", () => ({
|
|
17
|
+
isHttpAuthDisabled: () => fakeHttpAuthDisabled,
|
|
18
|
+
hasUngatedHttpAuthDisabled: () => false,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
// ── Real imports (after mocks) ───────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
import * as pendingInteractions from "../runtime/pending-interactions.js";
|
|
24
|
+
import { handleHostBrowserResult } from "../runtime/routes/host-browser-routes.js";
|
|
25
|
+
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
mock.restore();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
interface ResolveHostBrowserCall {
|
|
33
|
+
requestId: string;
|
|
34
|
+
response: { content: string; isError: boolean };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function makeStubConversation(spy: ResolveHostBrowserCall[]): Conversation {
|
|
38
|
+
return {
|
|
39
|
+
resolveHostBrowser(
|
|
40
|
+
requestId: string,
|
|
41
|
+
response: { content: string; isError: boolean },
|
|
42
|
+
) {
|
|
43
|
+
spy.push({ requestId, response });
|
|
44
|
+
},
|
|
45
|
+
} as unknown as Conversation;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const AUTHED_CONTEXT: AuthContext = {
|
|
49
|
+
subject: "actor:test",
|
|
50
|
+
principalType: "actor",
|
|
51
|
+
assistantId: "test-assistant",
|
|
52
|
+
actorPrincipalId: "actor-principal-1",
|
|
53
|
+
scopeProfile: "actor_client_v1",
|
|
54
|
+
scopes: new Set(),
|
|
55
|
+
policyEpoch: 0,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const UNAUTHED_CONTEXT: AuthContext = {
|
|
59
|
+
subject: "actor:test",
|
|
60
|
+
principalType: "actor",
|
|
61
|
+
assistantId: "test-assistant",
|
|
62
|
+
// actorPrincipalId intentionally absent
|
|
63
|
+
scopeProfile: "actor_client_v1",
|
|
64
|
+
scopes: new Set(),
|
|
65
|
+
policyEpoch: 0,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function makeJsonRequest(body: unknown): Request {
|
|
69
|
+
return new Request("http://localhost/v1/host-browser-result", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: { "Content-Type": "application/json" },
|
|
72
|
+
body: JSON.stringify(body),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Tests ────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
describe("handleHostBrowserResult", () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
pendingInteractions.clear();
|
|
81
|
+
fakeHttpAuthDisabled = true;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("happy path: resolves a pending host_browser interaction", async () => {
|
|
85
|
+
const spy: ResolveHostBrowserCall[] = [];
|
|
86
|
+
const conversation = makeStubConversation(spy);
|
|
87
|
+
const requestId = "browser-req-happy";
|
|
88
|
+
|
|
89
|
+
pendingInteractions.register(requestId, {
|
|
90
|
+
conversation,
|
|
91
|
+
conversationId: "conv-1",
|
|
92
|
+
kind: "host_browser",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const req = makeJsonRequest({
|
|
96
|
+
requestId,
|
|
97
|
+
content: "ok",
|
|
98
|
+
isError: false,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const res = await handleHostBrowserResult(req, AUTHED_CONTEXT);
|
|
102
|
+
const body = (await res.json()) as { accepted: boolean };
|
|
103
|
+
|
|
104
|
+
expect(res.status).toBe(200);
|
|
105
|
+
expect(body.accepted).toBe(true);
|
|
106
|
+
expect(spy).toHaveLength(1);
|
|
107
|
+
expect(spy[0].requestId).toBe(requestId);
|
|
108
|
+
expect(spy[0].response).toEqual({ content: "ok", isError: false });
|
|
109
|
+
|
|
110
|
+
// Pending interaction should be consumed
|
|
111
|
+
expect(pendingInteractions.get(requestId)).toBeUndefined();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("unauthorized: returns 403 when actor is not guardian-bound", async () => {
|
|
115
|
+
fakeHttpAuthDisabled = false;
|
|
116
|
+
|
|
117
|
+
const req = makeJsonRequest({
|
|
118
|
+
requestId: "browser-req-unauth",
|
|
119
|
+
content: "anything",
|
|
120
|
+
isError: false,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const res = await handleHostBrowserResult(req, UNAUTHED_CONTEXT);
|
|
124
|
+
expect(res.status).toBe(403);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("missing requestId: returns 400", async () => {
|
|
128
|
+
const req = makeJsonRequest({ content: "x" });
|
|
129
|
+
|
|
130
|
+
const res = await handleHostBrowserResult(req, AUTHED_CONTEXT);
|
|
131
|
+
expect(res.status).toBe(400);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("unknown requestId: returns 404", async () => {
|
|
135
|
+
const req = makeJsonRequest({
|
|
136
|
+
requestId: "00000000-0000-0000-0000-000000000000",
|
|
137
|
+
content: "x",
|
|
138
|
+
isError: false,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const res = await handleHostBrowserResult(req, AUTHED_CONTEXT);
|
|
142
|
+
expect(res.status).toBe(404);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("wrong kind: returns 409 with mismatch message", async () => {
|
|
146
|
+
const spy: ResolveHostBrowserCall[] = [];
|
|
147
|
+
const conversation = makeStubConversation(spy);
|
|
148
|
+
const requestId = "browser-req-wrong-kind";
|
|
149
|
+
|
|
150
|
+
pendingInteractions.register(requestId, {
|
|
151
|
+
conversation,
|
|
152
|
+
conversationId: "conv-1",
|
|
153
|
+
kind: "host_bash",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const req = makeJsonRequest({
|
|
157
|
+
requestId,
|
|
158
|
+
content: "x",
|
|
159
|
+
isError: false,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const res = await handleHostBrowserResult(req, AUTHED_CONTEXT);
|
|
163
|
+
expect(res.status).toBe(409);
|
|
164
|
+
|
|
165
|
+
const body = (await res.json()) as {
|
|
166
|
+
error: { message: string; code?: string };
|
|
167
|
+
};
|
|
168
|
+
expect(body.error.message).toContain('"host_bash"');
|
|
169
|
+
expect(body.error.message).toContain('"host_browser"');
|
|
170
|
+
|
|
171
|
+
// Pending interaction should NOT have been consumed
|
|
172
|
+
expect(pendingInteractions.get(requestId)).toBeDefined();
|
|
173
|
+
expect(spy).toHaveLength(0);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("defaults: missing content/isError default to '' and false", async () => {
|
|
177
|
+
const spy: ResolveHostBrowserCall[] = [];
|
|
178
|
+
const conversation = makeStubConversation(spy);
|
|
179
|
+
const requestId = "browser-req-defaults";
|
|
180
|
+
|
|
181
|
+
pendingInteractions.register(requestId, {
|
|
182
|
+
conversation,
|
|
183
|
+
conversationId: "conv-1",
|
|
184
|
+
kind: "host_browser",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const req = makeJsonRequest({ requestId });
|
|
188
|
+
|
|
189
|
+
const res = await handleHostBrowserResult(req, AUTHED_CONTEXT);
|
|
190
|
+
const body = (await res.json()) as { accepted: boolean };
|
|
191
|
+
|
|
192
|
+
expect(res.status).toBe(200);
|
|
193
|
+
expect(body.accepted).toBe(true);
|
|
194
|
+
expect(spy).toHaveLength(1);
|
|
195
|
+
expect(spy[0].requestId).toBe(requestId);
|
|
196
|
+
expect(spy[0].response).toEqual({ content: "", isError: false });
|
|
197
|
+
});
|
|
198
|
+
});
|