@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
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
1
|
+
import { afterEach, describe, expect, jest, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
const { HostFileProxy } = await import("../daemon/host-file-proxy.js");
|
|
4
4
|
|
|
5
|
+
// Minimal PNG header
|
|
6
|
+
const PNG_HEADER = Buffer.from([
|
|
7
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
|
|
8
|
+
0x48, 0x44, 0x52,
|
|
9
|
+
]);
|
|
10
|
+
|
|
5
11
|
describe("HostFileProxy", () => {
|
|
6
12
|
let proxy: InstanceType<typeof HostFileProxy>;
|
|
7
13
|
let sentMessages: unknown[];
|
|
@@ -77,6 +83,39 @@ describe("HostFileProxy", () => {
|
|
|
77
83
|
expect(result.content).toContain("ENOENT");
|
|
78
84
|
});
|
|
79
85
|
|
|
86
|
+
test("rebuilds image tool results from proxied image payloads", async () => {
|
|
87
|
+
setup();
|
|
88
|
+
|
|
89
|
+
const resultPromise = proxy.request(
|
|
90
|
+
{
|
|
91
|
+
operation: "read",
|
|
92
|
+
path: "/Users/test/Desktop/screenshot.png",
|
|
93
|
+
},
|
|
94
|
+
"session-1",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const sent = sentMessages[0] as Record<string, unknown>;
|
|
98
|
+
const requestId = sent.requestId as string;
|
|
99
|
+
|
|
100
|
+
proxy.resolve(requestId, {
|
|
101
|
+
content: "Image loaded on host",
|
|
102
|
+
isError: false,
|
|
103
|
+
imageData: PNG_HEADER.toString("base64"),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = await resultPromise;
|
|
107
|
+
expect(result.isError).toBe(false);
|
|
108
|
+
expect(result.content).toContain("Image loaded");
|
|
109
|
+
expect(result.content).toContain("/Users/test/Desktop/screenshot.png");
|
|
110
|
+
expect(result.contentBlocks).toHaveLength(1);
|
|
111
|
+
expect(result.contentBlocks?.[0]).toMatchObject({
|
|
112
|
+
type: "image",
|
|
113
|
+
source: {
|
|
114
|
+
media_type: "image/png",
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
80
119
|
test("handles write operations", async () => {
|
|
81
120
|
setup();
|
|
82
121
|
|
|
@@ -377,6 +416,151 @@ describe("HostFileProxy", () => {
|
|
|
377
416
|
});
|
|
378
417
|
});
|
|
379
418
|
|
|
419
|
+
describe("abort listener lifecycle", () => {
|
|
420
|
+
// Helper that wraps an AbortSignal to observe add/removeEventListener
|
|
421
|
+
// invocations without tripping over tsc's strict overload matching on
|
|
422
|
+
// AbortSignal itself.
|
|
423
|
+
type Spied = {
|
|
424
|
+
signal: AbortSignal;
|
|
425
|
+
addCalls: string[];
|
|
426
|
+
removeCalls: string[];
|
|
427
|
+
};
|
|
428
|
+
function spySignal(source: AbortSignal): Spied {
|
|
429
|
+
const addCalls: string[] = [];
|
|
430
|
+
const removeCalls: string[] = [];
|
|
431
|
+
|
|
432
|
+
const s = source as any;
|
|
433
|
+
const origAdd = source.addEventListener.bind(source);
|
|
434
|
+
const origRemove = source.removeEventListener.bind(source);
|
|
435
|
+
s.addEventListener = (
|
|
436
|
+
type: string,
|
|
437
|
+
|
|
438
|
+
...rest: any[]
|
|
439
|
+
) => {
|
|
440
|
+
addCalls.push(type);
|
|
441
|
+
|
|
442
|
+
return (origAdd as any)(type, ...rest);
|
|
443
|
+
};
|
|
444
|
+
s.removeEventListener = (
|
|
445
|
+
type: string,
|
|
446
|
+
|
|
447
|
+
...rest: any[]
|
|
448
|
+
) => {
|
|
449
|
+
removeCalls.push(type);
|
|
450
|
+
|
|
451
|
+
return (origRemove as any)(type, ...rest);
|
|
452
|
+
};
|
|
453
|
+
return { signal: source, addCalls, removeCalls };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
test("removes abort listener from signal after resolve completes", async () => {
|
|
457
|
+
setup();
|
|
458
|
+
const controller = new AbortController();
|
|
459
|
+
const spy = spySignal(controller.signal);
|
|
460
|
+
|
|
461
|
+
const resultPromise = proxy.request(
|
|
462
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
463
|
+
"session-1",
|
|
464
|
+
spy.signal,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
expect(spy.addCalls).toEqual(["abort"]);
|
|
468
|
+
expect(spy.removeCalls).toEqual([]);
|
|
469
|
+
|
|
470
|
+
const requestId = (sentMessages[0] as Record<string, unknown>)
|
|
471
|
+
.requestId as string;
|
|
472
|
+
proxy.resolve(requestId, { content: "file contents", isError: false });
|
|
473
|
+
await resultPromise;
|
|
474
|
+
|
|
475
|
+
// Listener is detached after normal completion.
|
|
476
|
+
expect(spy.removeCalls).toEqual(["abort"]);
|
|
477
|
+
|
|
478
|
+
// Subsequent aborts are harmless no-ops (no side effects on the proxy).
|
|
479
|
+
controller.abort();
|
|
480
|
+
// No additional emitted envelopes from the late abort.
|
|
481
|
+
expect(sentMessages).toHaveLength(1);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("removes abort listener from signal on timer timeout", async () => {
|
|
485
|
+
setup();
|
|
486
|
+
|
|
487
|
+
jest.useFakeTimers();
|
|
488
|
+
try {
|
|
489
|
+
const controller = new AbortController();
|
|
490
|
+
const spy = spySignal(controller.signal);
|
|
491
|
+
|
|
492
|
+
const resultPromise = proxy.request(
|
|
493
|
+
{ operation: "read", path: "/tmp/slow.txt" },
|
|
494
|
+
"session-1",
|
|
495
|
+
spy.signal,
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
expect(spy.addCalls).toEqual(["abort"]);
|
|
499
|
+
expect(spy.removeCalls).toEqual([]);
|
|
500
|
+
|
|
501
|
+
const requestId = (sentMessages[0] as Record<string, unknown>)
|
|
502
|
+
.requestId as string;
|
|
503
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(true);
|
|
504
|
+
|
|
505
|
+
// Advance past the 30s internal timeout.
|
|
506
|
+
jest.advanceTimersByTime(31 * 1000);
|
|
507
|
+
|
|
508
|
+
const result = await resultPromise;
|
|
509
|
+
expect(result.isError).toBe(true);
|
|
510
|
+
expect(result.content).toContain("Host file proxy timed out");
|
|
511
|
+
expect(proxy.hasPendingRequest(requestId)).toBe(false);
|
|
512
|
+
|
|
513
|
+
// Listener is detached after the timer fires.
|
|
514
|
+
expect(spy.removeCalls).toEqual(["abort"]);
|
|
515
|
+
|
|
516
|
+
// Subsequent aborts should be harmless — no cancel emitted.
|
|
517
|
+
controller.abort();
|
|
518
|
+
expect(sentMessages).toHaveLength(1);
|
|
519
|
+
} finally {
|
|
520
|
+
jest.useRealTimers();
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("sender throws synchronously", () => {
|
|
526
|
+
test("rejects the promise, clears pending state and timer, invokes onInternalResolve", async () => {
|
|
527
|
+
const resolvedIds: string[] = [];
|
|
528
|
+
sentMessages = [];
|
|
529
|
+
sendToClient = () => {
|
|
530
|
+
throw new Error("transport down");
|
|
531
|
+
};
|
|
532
|
+
proxy = new HostFileProxy(sendToClient, (id) => resolvedIds.push(id));
|
|
533
|
+
|
|
534
|
+
const resultPromise = proxy.request(
|
|
535
|
+
{ operation: "read", path: "/tmp/test.txt" },
|
|
536
|
+
"session-1",
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
await expect(resultPromise).rejects.toThrow("transport down");
|
|
540
|
+
|
|
541
|
+
// The internal resolve should fire exactly once as part of cleanup.
|
|
542
|
+
expect(resolvedIds).toHaveLength(1);
|
|
543
|
+
|
|
544
|
+
// Issue a new request on a fresh (non-throwing) sender and verify
|
|
545
|
+
// the proxy is still functional — no stale timers or bookkeeping
|
|
546
|
+
// from the failed request.
|
|
547
|
+
sentMessages = [];
|
|
548
|
+
proxy.updateSender((msg) => sentMessages.push(msg), true);
|
|
549
|
+
const okPromise = proxy.request(
|
|
550
|
+
{ operation: "read", path: "/tmp/ok.txt" },
|
|
551
|
+
"session-1",
|
|
552
|
+
);
|
|
553
|
+
expect(sentMessages).toHaveLength(1);
|
|
554
|
+
const okRequestId = (sentMessages[0] as Record<string, unknown>)
|
|
555
|
+
.requestId as string;
|
|
556
|
+
expect(proxy.hasPendingRequest(okRequestId)).toBe(true);
|
|
557
|
+
proxy.resolve(okRequestId, { content: "ok", isError: false });
|
|
558
|
+
const okResult = await okPromise;
|
|
559
|
+
expect(okResult.content).toBe("ok");
|
|
560
|
+
expect(okResult.isError).toBe(false);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
|
|
380
564
|
describe("onInternalResolve callback", () => {
|
|
381
565
|
test("fires on abort", async () => {
|
|
382
566
|
const resolvedIds: string[] = [];
|
|
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
5
5
|
|
|
6
|
+
import type { HostFileInput } from "../daemon/host-file-proxy.js";
|
|
6
7
|
import { hostFileReadTool } from "../tools/host-filesystem/read.js";
|
|
7
8
|
import type { ToolContext } from "../tools/types.js";
|
|
8
9
|
|
|
@@ -165,6 +166,57 @@ describe("host_file_read tool", () => {
|
|
|
165
166
|
});
|
|
166
167
|
|
|
167
168
|
describe("host_file_read image support", () => {
|
|
169
|
+
test("uses host proxy for image reads when available", async () => {
|
|
170
|
+
const requests: Array<{
|
|
171
|
+
input: HostFileInput;
|
|
172
|
+
conversationId: string;
|
|
173
|
+
signal?: AbortSignal;
|
|
174
|
+
}> = [];
|
|
175
|
+
const proxyContext: ToolContext = {
|
|
176
|
+
...makeContext(),
|
|
177
|
+
hostFileProxy: {
|
|
178
|
+
isAvailable: () => true,
|
|
179
|
+
request: async (input, conversationId, signal) => {
|
|
180
|
+
requests.push({ input, conversationId, signal });
|
|
181
|
+
return {
|
|
182
|
+
content: "Image loaded: /host/screenshot.png",
|
|
183
|
+
isError: false,
|
|
184
|
+
contentBlocks: [
|
|
185
|
+
{
|
|
186
|
+
type: "image",
|
|
187
|
+
source: {
|
|
188
|
+
type: "base64",
|
|
189
|
+
media_type: "image/png",
|
|
190
|
+
data: PNG_HEADER.toString("base64"),
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
},
|
|
196
|
+
} as ToolContext["hostFileProxy"],
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const result = await hostFileReadTool.execute(
|
|
200
|
+
{ path: "/host/screenshot.png" },
|
|
201
|
+
proxyContext,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
expect(result.isError).toBe(false);
|
|
205
|
+
expect(result.contentBlocks).toHaveLength(1);
|
|
206
|
+
expect(requests).toEqual([
|
|
207
|
+
{
|
|
208
|
+
input: {
|
|
209
|
+
operation: "read",
|
|
210
|
+
path: "/host/screenshot.png",
|
|
211
|
+
offset: undefined,
|
|
212
|
+
limit: undefined,
|
|
213
|
+
},
|
|
214
|
+
conversationId: "test-conversation",
|
|
215
|
+
signal: undefined,
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
});
|
|
219
|
+
|
|
168
220
|
test("returns image content block for .png file", async () => {
|
|
169
221
|
const dir = makeTempDir();
|
|
170
222
|
const filePath = join(dir, "screenshot.png");
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { HostProxyInterfaceId, InterfaceId } from "../channels/types.js";
|
|
4
|
+
import { supportsHostProxy } from "../channels/types.js";
|
|
5
|
+
import type {
|
|
6
|
+
ConversationTransportMetadata,
|
|
7
|
+
HostProxyTransportMetadata,
|
|
8
|
+
NonHostProxyTransportMetadata,
|
|
9
|
+
} from "../daemon/message-types/conversations.js";
|
|
10
|
+
import { isHostProxyTransport } from "../daemon/message-types/conversations.js";
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// supportsHostProxy — runtime behavior
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
describe("supportsHostProxy (runtime)", () => {
|
|
17
|
+
test("no-arg form returns true for host-proxy interfaces", () => {
|
|
18
|
+
expect(supportsHostProxy("macos")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("no-arg form returns false for interfaces without host-proxy support", () => {
|
|
22
|
+
const nonHostProxyIds: InterfaceId[] = [
|
|
23
|
+
"ios",
|
|
24
|
+
"cli",
|
|
25
|
+
"telegram",
|
|
26
|
+
"phone",
|
|
27
|
+
"vellum",
|
|
28
|
+
"whatsapp",
|
|
29
|
+
"slack",
|
|
30
|
+
"email",
|
|
31
|
+
"chrome-extension",
|
|
32
|
+
];
|
|
33
|
+
for (const id of nonHostProxyIds) {
|
|
34
|
+
expect(supportsHostProxy(id)).toBe(false);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("capability form grants host_browser to chrome-extension", () => {
|
|
39
|
+
expect(supportsHostProxy("chrome-extension", "host_browser")).toBe(true);
|
|
40
|
+
expect(supportsHostProxy("chrome-extension", "host_bash")).toBe(false);
|
|
41
|
+
expect(supportsHostProxy("chrome-extension", "host_file")).toBe(false);
|
|
42
|
+
expect(supportsHostProxy("chrome-extension", "host_cu")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("capability form grants host_bash/file/cu to macOS but not host_browser", () => {
|
|
46
|
+
expect(supportsHostProxy("macos", "host_bash")).toBe(true);
|
|
47
|
+
expect(supportsHostProxy("macos", "host_file")).toBe(true);
|
|
48
|
+
expect(supportsHostProxy("macos", "host_cu")).toBe(true);
|
|
49
|
+
expect(supportsHostProxy("macos", "host_browser")).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("capability form rejects everything for non-host-proxy interfaces", () => {
|
|
53
|
+
expect(supportsHostProxy("ios", "host_bash")).toBe(false);
|
|
54
|
+
expect(supportsHostProxy("cli", "host_file")).toBe(false);
|
|
55
|
+
expect(supportsHostProxy("telegram", "host_browser")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// supportsHostProxy — type predicate (compile-time contract)
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
describe("supportsHostProxy (type predicate)", () => {
|
|
64
|
+
test("no-arg form narrows InterfaceId to HostProxyInterfaceId", () => {
|
|
65
|
+
const id: InterfaceId = "macos";
|
|
66
|
+
if (supportsHostProxy(id)) {
|
|
67
|
+
// Inside this branch, TypeScript narrows `id` to HostProxyInterfaceId.
|
|
68
|
+
// If the overload were wrong, this assignment would fail to type-check
|
|
69
|
+
// and the test file wouldn't compile.
|
|
70
|
+
const narrowed: HostProxyInterfaceId = id;
|
|
71
|
+
expect(narrowed).toBe("macos");
|
|
72
|
+
} else {
|
|
73
|
+
throw new Error("expected narrowing branch to be taken for macos");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("narrowing reaches through discriminated transport union", () => {
|
|
78
|
+
// Build a value typed as the full union so TypeScript can't cheat.
|
|
79
|
+
const transport: ConversationTransportMetadata = {
|
|
80
|
+
channelId: "vellum",
|
|
81
|
+
interfaceId: "macos",
|
|
82
|
+
hostHomeDir: "/Users/alice",
|
|
83
|
+
hostUsername: "alice",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (transport.interfaceId && supportsHostProxy(transport.interfaceId)) {
|
|
87
|
+
// Narrowing the discriminant narrows the union member — after this
|
|
88
|
+
// check, `transport` should be HostProxyTransportMetadata and the
|
|
89
|
+
// host-env fields are directly accessible.
|
|
90
|
+
const narrowed: HostProxyTransportMetadata = transport;
|
|
91
|
+
expect(narrowed.hostHomeDir).toBe("/Users/alice");
|
|
92
|
+
expect(narrowed.hostUsername).toBe("alice");
|
|
93
|
+
} else {
|
|
94
|
+
throw new Error("expected host-proxy branch for macos transport");
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("non-host-proxy branch narrows to NonHostProxyTransportMetadata", () => {
|
|
99
|
+
const transport: ConversationTransportMetadata = {
|
|
100
|
+
channelId: "vellum",
|
|
101
|
+
interfaceId: "ios",
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
if (transport.interfaceId && supportsHostProxy(transport.interfaceId)) {
|
|
105
|
+
throw new Error("expected non-host-proxy branch for ios transport");
|
|
106
|
+
} else {
|
|
107
|
+
// `transport` is NonHostProxyTransportMetadata here.
|
|
108
|
+
const narrowed: NonHostProxyTransportMetadata = transport;
|
|
109
|
+
expect(narrowed.interfaceId).toBe("ios");
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// isHostProxyTransport — type guard on ConversationTransportMetadata
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
describe("isHostProxyTransport", () => {
|
|
119
|
+
test("returns true for macOS transport and narrows to HostProxyTransportMetadata", () => {
|
|
120
|
+
const transport: ConversationTransportMetadata = {
|
|
121
|
+
channelId: "vellum",
|
|
122
|
+
interfaceId: "macos",
|
|
123
|
+
hostHomeDir: "/Users/alice",
|
|
124
|
+
hostUsername: "alice",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
expect(isHostProxyTransport(transport)).toBe(true);
|
|
128
|
+
|
|
129
|
+
if (isHostProxyTransport(transport)) {
|
|
130
|
+
const narrowed: HostProxyTransportMetadata = transport;
|
|
131
|
+
expect(narrowed.hostHomeDir).toBe("/Users/alice");
|
|
132
|
+
expect(narrowed.hostUsername).toBe("alice");
|
|
133
|
+
} else {
|
|
134
|
+
throw new Error("narrowing branch not taken");
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("returns false for every non-host-proxy interface", () => {
|
|
139
|
+
const nonHostProxyIds: Array<Exclude<InterfaceId, HostProxyInterfaceId>> = [
|
|
140
|
+
"ios",
|
|
141
|
+
"cli",
|
|
142
|
+
"telegram",
|
|
143
|
+
"phone",
|
|
144
|
+
"vellum",
|
|
145
|
+
"whatsapp",
|
|
146
|
+
"slack",
|
|
147
|
+
"email",
|
|
148
|
+
"chrome-extension",
|
|
149
|
+
];
|
|
150
|
+
for (const interfaceId of nonHostProxyIds) {
|
|
151
|
+
const transport: ConversationTransportMetadata = {
|
|
152
|
+
channelId: "vellum",
|
|
153
|
+
interfaceId,
|
|
154
|
+
};
|
|
155
|
+
expect(isHostProxyTransport(transport)).toBe(false);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("returns false when interfaceId is absent", () => {
|
|
160
|
+
const transport: ConversationTransportMetadata = {
|
|
161
|
+
channelId: "vellum",
|
|
162
|
+
};
|
|
163
|
+
expect(isHostProxyTransport(transport)).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -36,7 +36,6 @@ const mockConfig = {
|
|
|
36
36
|
entropyThreshold: 4.0,
|
|
37
37
|
},
|
|
38
38
|
auditLog: { retentionDays: 0 },
|
|
39
|
-
sandbox: { enabled: true },
|
|
40
39
|
};
|
|
41
40
|
|
|
42
41
|
// Track whether wrapCommand was ever called — host_bash must never invoke it
|
|
@@ -149,10 +148,6 @@ describe("host_bash tool", () => {
|
|
|
149
148
|
const dir = mkdtempSync(join(tmpdir(), "host-shell-plain-"));
|
|
150
149
|
testDirs.push(dir);
|
|
151
150
|
|
|
152
|
-
// Verify the tool executes successfully even when sandbox is enabled in config,
|
|
153
|
-
// proving it bypasses the sandbox entirely
|
|
154
|
-
expect(mockConfig.sandbox.enabled).toBe(true);
|
|
155
|
-
|
|
156
151
|
spawnCalls.length = 0;
|
|
157
152
|
|
|
158
153
|
const result = await hostShellTool.execute(
|
|
@@ -236,10 +231,7 @@ describe("host_bash — baseline: no sandbox isolation", () => {
|
|
|
236
231
|
expect(spawnCalls[0].args[2]).toBe("ls -la /tmp");
|
|
237
232
|
});
|
|
238
233
|
|
|
239
|
-
test("
|
|
240
|
-
// The mock config has sandbox.enabled = true
|
|
241
|
-
expect(mockConfig.sandbox.enabled).toBe(true);
|
|
242
|
-
|
|
234
|
+
test("host_bash always spawns plain bash without wrapCommand", async () => {
|
|
243
235
|
const dir = mkdtempSync(join(tmpdir(), "host-shell-sandbox-cfg-"));
|
|
244
236
|
testDirs.push(dir);
|
|
245
237
|
|
|
@@ -255,9 +247,7 @@ describe("host_bash — baseline: no sandbox isolation", () => {
|
|
|
255
247
|
);
|
|
256
248
|
|
|
257
249
|
expect(result.isError).toBe(false);
|
|
258
|
-
// Must never call wrapCommand regardless of config
|
|
259
250
|
expect(wrapCommandCallCount).toBe(0);
|
|
260
|
-
// Must still spawn plain bash
|
|
261
251
|
expect(spawnCalls[0].command).toBe("bash");
|
|
262
252
|
});
|
|
263
253
|
});
|
|
@@ -178,6 +178,7 @@ function makeConversation(overrides: Record<string, unknown> = {}) {
|
|
|
178
178
|
setTrustContext: () => {},
|
|
179
179
|
updateClient: () => {},
|
|
180
180
|
setHostBashProxy: () => {},
|
|
181
|
+
setHostBrowserProxy: () => {},
|
|
181
182
|
setHostFileProxy: () => {},
|
|
182
183
|
setHostCuProxy: () => {},
|
|
183
184
|
addPreactivatedSkillId: () => {},
|
|
@@ -21,17 +21,16 @@ mock.module("../config/loader.js", () => ({
|
|
|
21
21
|
}));
|
|
22
22
|
|
|
23
23
|
mock.module("../oauth/oauth-store.js", () => ({
|
|
24
|
-
isProviderConnected: (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
? { id: `conn-${providerKey}`, status: "active" }
|
|
24
|
+
isProviderConnected: (provider: string) => connectedProviders.has(provider),
|
|
25
|
+
getConnectionByProvider: (provider: string) =>
|
|
26
|
+
connectedProviders.has(provider)
|
|
27
|
+
? { id: `conn-${provider}`, status: "active" }
|
|
29
28
|
: undefined,
|
|
30
29
|
}));
|
|
31
30
|
|
|
32
31
|
/** Mark a provider as fully connected (active row + access token). */
|
|
33
|
-
function setOAuthConnected(
|
|
34
|
-
connectedProviders.add(
|
|
32
|
+
function setOAuthConnected(provider: string): void {
|
|
33
|
+
connectedProviders.add(provider);
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
const { getIntegrationSummary, formatIntegrationSummary, hasCapability } =
|
|
@@ -84,7 +84,11 @@ describe("handleListMessages tool_result merging", () => {
|
|
|
84
84
|
conv.id,
|
|
85
85
|
"user",
|
|
86
86
|
JSON.stringify([
|
|
87
|
-
{
|
|
87
|
+
{
|
|
88
|
+
type: "tool_result",
|
|
89
|
+
tool_use_id: "tu1",
|
|
90
|
+
content: "file1.txt\nfile2.txt",
|
|
91
|
+
},
|
|
88
92
|
]),
|
|
89
93
|
);
|
|
90
94
|
|
|
@@ -117,7 +121,12 @@ describe("handleListMessages tool_result merging", () => {
|
|
|
117
121
|
JSON.stringify([
|
|
118
122
|
{ type: "tool_use", id: "tu1", name: "bash", input: { command: "ls" } },
|
|
119
123
|
{ type: "text", text: "and also" },
|
|
120
|
-
{
|
|
124
|
+
{
|
|
125
|
+
type: "tool_use",
|
|
126
|
+
id: "tu2",
|
|
127
|
+
name: "file_read",
|
|
128
|
+
input: { path: "/tmp/a" },
|
|
129
|
+
},
|
|
121
130
|
]),
|
|
122
131
|
);
|
|
123
132
|
await addMessage(
|
|
@@ -176,7 +185,11 @@ describe("handleListMessages tool_result merging", () => {
|
|
|
176
185
|
conv.id,
|
|
177
186
|
"user",
|
|
178
187
|
JSON.stringify([
|
|
179
|
-
{
|
|
188
|
+
{
|
|
189
|
+
type: "tool_result",
|
|
190
|
+
tool_use_id: "tu_orphan",
|
|
191
|
+
content: "stale result",
|
|
192
|
+
},
|
|
180
193
|
]),
|
|
181
194
|
);
|
|
182
195
|
await addMessage(
|
|
@@ -228,7 +241,12 @@ describe("handleListMessages tool_result merging", () => {
|
|
|
228
241
|
"assistant",
|
|
229
242
|
JSON.stringify([
|
|
230
243
|
{ type: "text", text: "Now reading:" },
|
|
231
|
-
{
|
|
244
|
+
{
|
|
245
|
+
type: "tool_use",
|
|
246
|
+
id: "tu2",
|
|
247
|
+
name: "file_read",
|
|
248
|
+
input: { path: "/x" },
|
|
249
|
+
},
|
|
232
250
|
]),
|
|
233
251
|
);
|
|
234
252
|
await addMessage(
|
|
@@ -248,17 +266,19 @@ describe("handleListMessages tool_result merging", () => {
|
|
|
248
266
|
const response = handleListMessages(createTestUrl(conv.id), null);
|
|
249
267
|
const body = (await response.json()) as { messages: MessagePayload[] };
|
|
250
268
|
|
|
251
|
-
//
|
|
252
|
-
|
|
269
|
+
// Consecutive assistant messages are merged at query time so the client
|
|
270
|
+
// sees one grouped message (matching the streaming path behavior).
|
|
271
|
+
// user("list files"), merged-assistant(bash + file_read), user("thanks")
|
|
272
|
+
expect(body.messages).toHaveLength(3);
|
|
253
273
|
expect(body.messages[0].role).toBe("user");
|
|
254
274
|
expect(body.messages[1].role).toBe("assistant");
|
|
275
|
+
expect(body.messages[1].toolCalls).toHaveLength(2);
|
|
255
276
|
expect(body.messages[1].toolCalls![0].name).toBe("bash");
|
|
256
277
|
expect(body.messages[1].toolCalls![0].result).toBe("files");
|
|
257
|
-
expect(body.messages[
|
|
258
|
-
expect(body.messages[
|
|
259
|
-
expect(body.messages[2].
|
|
260
|
-
expect(body.messages[
|
|
261
|
-
expect(body.messages[3].content).toBe("thanks");
|
|
278
|
+
expect(body.messages[1].toolCalls![1].name).toBe("file_read");
|
|
279
|
+
expect(body.messages[1].toolCalls![1].result).toBe("file data");
|
|
280
|
+
expect(body.messages[2].role).toBe("user");
|
|
281
|
+
expect(body.messages[2].content).toBe("thanks");
|
|
262
282
|
});
|
|
263
283
|
|
|
264
284
|
test("tool_result with is_error propagates error status", async () => {
|
|
@@ -272,7 +292,12 @@ describe("handleListMessages tool_result merging", () => {
|
|
|
272
292
|
conv.id,
|
|
273
293
|
"assistant",
|
|
274
294
|
JSON.stringify([
|
|
275
|
-
{
|
|
295
|
+
{
|
|
296
|
+
type: "tool_use",
|
|
297
|
+
id: "tu1",
|
|
298
|
+
name: "bash",
|
|
299
|
+
input: { command: "fail" },
|
|
300
|
+
},
|
|
276
301
|
]),
|
|
277
302
|
);
|
|
278
303
|
await addMessage(
|
|
@@ -13,6 +13,7 @@ mock.module("../inbound/platform-callback-registration.js", () => ({
|
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
15
|
const { McpClient } = await import("../mcp/client.js");
|
|
16
|
+
const { McpOAuthProvider } = await import("../mcp/mcp-oauth-provider.js");
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Mimics the SDK's StreamableHTTPError which has a `.code` property
|
|
@@ -67,7 +68,7 @@ describe("McpClient auth error detection", () => {
|
|
|
67
68
|
expect(client.isConnected).toBe(false);
|
|
68
69
|
});
|
|
69
70
|
|
|
70
|
-
test("
|
|
71
|
+
test("swallows non-auth StreamableHTTPError (connect never throws)", async () => {
|
|
71
72
|
const client = new McpClient("test-server");
|
|
72
73
|
|
|
73
74
|
(client as any).createTransport = () => ({});
|
|
@@ -78,9 +79,9 @@ describe("McpClient auth error detection", () => {
|
|
|
78
79
|
close: async () => {},
|
|
79
80
|
};
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
82
|
+
// Non-auth errors are logged but never propagated — daemon keeps running
|
|
83
|
+
await client.connect(httpTransport);
|
|
84
|
+
expect(client.isConnected).toBe(false);
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
test("treats error message containing 'unauthorized' as auth error", async () => {
|
|
@@ -97,4 +98,39 @@ describe("McpClient auth error detection", () => {
|
|
|
97
98
|
await client.connect(httpTransport);
|
|
98
99
|
expect(client.isConnected).toBe(false);
|
|
99
100
|
});
|
|
101
|
+
|
|
102
|
+
test("treats SDK fetchToken 'authorizationCode is required' error as auth error", async () => {
|
|
103
|
+
const client = new McpClient("test-server");
|
|
104
|
+
|
|
105
|
+
(client as any).createTransport = () => ({});
|
|
106
|
+
(client as any).client = {
|
|
107
|
+
connect: () => {
|
|
108
|
+
throw new Error(
|
|
109
|
+
"Either provider.prepareTokenRequest() or authorizationCode is required",
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
close: async () => {},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
await client.connect(httpTransport);
|
|
116
|
+
expect(client.isConnected).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("McpOAuthProvider redirectUrl", () => {
|
|
121
|
+
test("redirectUrl is undefined until startCallbackServer() is called", () => {
|
|
122
|
+
const nonInteractive = new McpOAuthProvider(
|
|
123
|
+
"test-server",
|
|
124
|
+
"https://example.com/mcp",
|
|
125
|
+
/* interactive */ false,
|
|
126
|
+
);
|
|
127
|
+
expect(nonInteractive.redirectUrl).toBeUndefined();
|
|
128
|
+
|
|
129
|
+
const interactive = new McpOAuthProvider(
|
|
130
|
+
"test-server",
|
|
131
|
+
"https://example.com/mcp",
|
|
132
|
+
/* interactive */ true,
|
|
133
|
+
);
|
|
134
|
+
expect(interactive.redirectUrl).toBeUndefined();
|
|
135
|
+
});
|
|
100
136
|
});
|