@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
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
12
12
|
|
|
13
|
+
import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
|
|
14
|
+
|
|
13
15
|
mock.module("../util/logger.js", () => ({
|
|
14
16
|
getLogger: () =>
|
|
15
17
|
new Proxy({} as Record<string, unknown>, {
|
|
@@ -133,6 +135,7 @@ describe("bridgeConfirmationRequestToGuardian", () => {
|
|
|
133
135
|
resetTables();
|
|
134
136
|
emittedSignals.length = 0;
|
|
135
137
|
mockOnConversationCreatedCallbacks.length = 0;
|
|
138
|
+
_setOverridesForTesting({});
|
|
136
139
|
});
|
|
137
140
|
|
|
138
141
|
test("emits guardian.question for trusted-contact sessions", () => {
|
|
@@ -223,6 +226,26 @@ describe("bridgeConfirmationRequestToGuardian", () => {
|
|
|
223
226
|
expect(emittedSignals).toHaveLength(0);
|
|
224
227
|
});
|
|
225
228
|
|
|
229
|
+
test("skips trusted-contact bridging entirely under permission-controls-v2", () => {
|
|
230
|
+
_setOverridesForTesting({ "permission-controls-v2": true });
|
|
231
|
+
|
|
232
|
+
const canonicalRequest = makeCanonicalRequest();
|
|
233
|
+
const trustContext = makeTrustedContactContext();
|
|
234
|
+
|
|
235
|
+
const result = bridgeConfirmationRequestToGuardian({
|
|
236
|
+
canonicalRequest,
|
|
237
|
+
trustContext,
|
|
238
|
+
conversationId: "conv-1",
|
|
239
|
+
toolName: "bash",
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect("skipped" in result && result.skipped).toBe(true);
|
|
243
|
+
if ("skipped" in result) {
|
|
244
|
+
expect(result.reason).toBe("v2_model_mediated");
|
|
245
|
+
}
|
|
246
|
+
expect(emittedSignals).toHaveLength(0);
|
|
247
|
+
});
|
|
248
|
+
|
|
226
249
|
test("skips when no guardian binding exists for channel", () => {
|
|
227
250
|
const canonicalRequest = makeCanonicalRequest({ sourceChannel: "phone" });
|
|
228
251
|
const trustContext = makeTrustedContactContext({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { describe, expect, mock, test } from "bun:test";
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
|
|
3
4
|
import {
|
|
4
5
|
CONTEXT_OVERFLOW_TOOL_NAME,
|
|
5
6
|
requestCompressionApproval,
|
|
@@ -20,8 +21,22 @@ function createMockPrompter(decision: UserDecision): PermissionPrompter {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
describe("requestCompressionApproval", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
_setOverridesForTesting({});
|
|
26
|
+
});
|
|
27
|
+
|
|
23
28
|
// ── Prompt shape ──
|
|
24
29
|
|
|
30
|
+
test("auto-approves without prompting under v2", async () => {
|
|
31
|
+
_setOverridesForTesting({ "permission-controls-v2": true });
|
|
32
|
+
|
|
33
|
+
const prompter = createMockPrompter("deny");
|
|
34
|
+
const result = await requestCompressionApproval(prompter);
|
|
35
|
+
|
|
36
|
+
expect(result).toEqual({ approved: true });
|
|
37
|
+
expect(prompter.prompt).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
25
40
|
test("uses the reserved pseudo tool name", async () => {
|
|
26
41
|
const prompter = createMockPrompter("allow");
|
|
27
42
|
await requestCompressionApproval(prompter);
|
|
@@ -212,7 +212,7 @@ mock.module("../daemon/conversation-runtime-assembly.js", () => ({
|
|
|
212
212
|
}));
|
|
213
213
|
|
|
214
214
|
mock.module("../daemon/date-context.js", () => ({
|
|
215
|
-
formatTurnTimestamp: () => "2026-01-01 (
|
|
215
|
+
formatTurnTimestamp: () => "2026-01-01 (Thursday) 00:00:00 +00:00 (UTC)",
|
|
216
216
|
}));
|
|
217
217
|
|
|
218
218
|
mock.module("../daemon/history-repair.js", () => ({
|
|
@@ -201,7 +201,7 @@ mock.module("../daemon/conversation-runtime-assembly.js", () => ({
|
|
|
201
201
|
}));
|
|
202
202
|
|
|
203
203
|
mock.module("../daemon/date-context.js", () => ({
|
|
204
|
-
formatTurnTimestamp: () => "2026-01-01 (
|
|
204
|
+
formatTurnTimestamp: () => "2026-01-01 (Thursday) 00:00:00 +00:00 (UTC)",
|
|
205
205
|
}));
|
|
206
206
|
|
|
207
207
|
mock.module("../daemon/history-repair.js", () => ({
|
|
@@ -121,7 +121,7 @@ describe("POST /v1/conversations/:id/analyze", () => {
|
|
|
121
121
|
);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
test("
|
|
124
|
+
test("keeps analysis non-interactive even when a matching subscriber is connected", async () => {
|
|
125
125
|
const conversation = makeConversation();
|
|
126
126
|
const assistantEventHub = new AssistantEventHub();
|
|
127
127
|
assistantEventHub.subscribe(
|
|
@@ -163,7 +163,7 @@ describe("POST /v1/conversations/:id/analyze", () => {
|
|
|
163
163
|
expect.any(String),
|
|
164
164
|
"msg-1",
|
|
165
165
|
expect.any(Function),
|
|
166
|
-
expect.objectContaining({ isInteractive:
|
|
166
|
+
expect.objectContaining({ isInteractive: false, isUserMessage: true }),
|
|
167
167
|
);
|
|
168
168
|
});
|
|
169
169
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
3
3
|
|
|
4
|
+
import { _setOverridesForTesting } from "../config/assistant-feature-flags.js";
|
|
5
|
+
|
|
4
6
|
mock.module("../util/logger.js", () => ({
|
|
5
7
|
getLogger: () =>
|
|
6
8
|
new Proxy({} as Record<string, unknown>, {
|
|
@@ -25,15 +27,18 @@ mock.module("../daemon/video-thumbnail.js", () => ({
|
|
|
25
27
|
}));
|
|
26
28
|
|
|
27
29
|
// Stub out permission checker / trust store
|
|
30
|
+
const checkSpy = mock(() => Promise.resolve({ decision: "allow" }));
|
|
31
|
+
const addRuleSpy = mock(() => {});
|
|
32
|
+
|
|
28
33
|
mock.module("../permissions/checker.js", () => ({
|
|
29
|
-
check:
|
|
34
|
+
check: checkSpy,
|
|
30
35
|
classifyRisk: () => Promise.resolve("low"),
|
|
31
36
|
generateAllowlistOptions: () => Promise.resolve([]),
|
|
32
37
|
generateScopeOptions: () => [],
|
|
33
38
|
}));
|
|
34
39
|
|
|
35
40
|
mock.module("../permissions/trust-store.js", () => ({
|
|
36
|
-
addRule:
|
|
41
|
+
addRule: addRuleSpy,
|
|
37
42
|
}));
|
|
38
43
|
|
|
39
44
|
mock.module("../permissions/types.js", () => ({
|
|
@@ -47,7 +52,11 @@ mock.module("../permissions/types.js", () => ({
|
|
|
47
52
|
|
|
48
53
|
import type { AssistantAttachmentDraft } from "../daemon/assistant-attachments.js";
|
|
49
54
|
import { getFilePathForAttachment } from "../memory/attachments-store.js";
|
|
50
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
addMessage,
|
|
57
|
+
createConversation,
|
|
58
|
+
updateConversationHostAccess,
|
|
59
|
+
} from "../memory/conversation-crud.js";
|
|
51
60
|
import { getDb, initializeDb } from "../memory/db.js";
|
|
52
61
|
|
|
53
62
|
initializeDb();
|
|
@@ -73,7 +82,12 @@ function makeBase64(bytes: number): string {
|
|
|
73
82
|
// ---------------------------------------------------------------------------
|
|
74
83
|
|
|
75
84
|
describe("resolveAssistantAttachments", () => {
|
|
76
|
-
beforeEach(
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
resetTables();
|
|
87
|
+
_setOverridesForTesting({});
|
|
88
|
+
checkSpy.mockClear();
|
|
89
|
+
addRuleSpy.mockClear();
|
|
90
|
+
});
|
|
77
91
|
|
|
78
92
|
test("small attachments are stored on disk via uploadAttachment", async () => {
|
|
79
93
|
const conv = createConversation("test-conv");
|
|
@@ -196,3 +210,65 @@ describe("resolveAssistantAttachments", () => {
|
|
|
196
210
|
expect(emitted.fileBacked).toBe(true);
|
|
197
211
|
});
|
|
198
212
|
});
|
|
213
|
+
|
|
214
|
+
describe("approveHostAttachmentRead", () => {
|
|
215
|
+
beforeEach(() => {
|
|
216
|
+
resetTables();
|
|
217
|
+
_setOverridesForTesting({});
|
|
218
|
+
checkSpy.mockClear();
|
|
219
|
+
addRuleSpy.mockClear();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("uses the conversation-scoped host-access prompt under v2", async () => {
|
|
223
|
+
_setOverridesForTesting({ "permission-controls-v2": true });
|
|
224
|
+
const conversation = createConversation("attachment-host-gate");
|
|
225
|
+
const promptSpy = mock(() =>
|
|
226
|
+
Promise.resolve({ decision: "allow" as const }),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const { approveHostAttachmentRead } =
|
|
230
|
+
await import("../daemon/conversation-attachments.js");
|
|
231
|
+
|
|
232
|
+
const allowed = await approveHostAttachmentRead(
|
|
233
|
+
"/tmp/example.txt",
|
|
234
|
+
"/tmp",
|
|
235
|
+
{ prompt: promptSpy } as never,
|
|
236
|
+
conversation.id,
|
|
237
|
+
false,
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
expect(allowed).toBe(true);
|
|
241
|
+
expect(checkSpy).not.toHaveBeenCalled();
|
|
242
|
+
expect(addRuleSpy).not.toHaveBeenCalled();
|
|
243
|
+
const call = promptSpy.mock.calls[0] as unknown as unknown[];
|
|
244
|
+
expect(call[3]).toEqual([]);
|
|
245
|
+
expect(call[4]).toEqual([]);
|
|
246
|
+
expect(call[8]).toBe(false);
|
|
247
|
+
expect(call[10]).toBeUndefined();
|
|
248
|
+
expect(call[12]).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("auto-allows host attachment reads when the conversation already has host access", async () => {
|
|
252
|
+
_setOverridesForTesting({ "permission-controls-v2": true });
|
|
253
|
+
const conversation = createConversation("attachment-host-allowed");
|
|
254
|
+
updateConversationHostAccess(conversation.id, true);
|
|
255
|
+
const promptSpy = mock(() =>
|
|
256
|
+
Promise.resolve({ decision: "deny" as const }),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const { approveHostAttachmentRead } =
|
|
260
|
+
await import("../daemon/conversation-attachments.js");
|
|
261
|
+
|
|
262
|
+
const allowed = await approveHostAttachmentRead(
|
|
263
|
+
"/tmp/example.txt",
|
|
264
|
+
"/tmp",
|
|
265
|
+
{ prompt: promptSpy } as never,
|
|
266
|
+
conversation.id,
|
|
267
|
+
false,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
expect(allowed).toBe(true);
|
|
271
|
+
expect(promptSpy).not.toHaveBeenCalled();
|
|
272
|
+
expect(checkSpy).not.toHaveBeenCalled();
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -214,6 +214,8 @@ mock.module("../memory/canonical-guardian-store.js", () => ({
|
|
|
214
214
|
// ---------------------------------------------------------------------------
|
|
215
215
|
|
|
216
216
|
import { Conversation } from "../daemon/conversation.js";
|
|
217
|
+
import { HostBashProxy } from "../daemon/host-bash-proxy.js";
|
|
218
|
+
import { HostBrowserProxy } from "../daemon/host-browser-proxy.js";
|
|
217
219
|
|
|
218
220
|
// ---------------------------------------------------------------------------
|
|
219
221
|
// Helpers
|
|
@@ -558,3 +560,156 @@ describe("sendToClient receives state signals", () => {
|
|
|
558
560
|
});
|
|
559
561
|
});
|
|
560
562
|
});
|
|
563
|
+
|
|
564
|
+
describe("restoreBrowserProxyAvailability", () => {
|
|
565
|
+
test("re-enables only the host browser proxy after clearProxyAvailability", () => {
|
|
566
|
+
const conversation = makeConversation();
|
|
567
|
+
const browserProxy = new HostBrowserProxy(() => {});
|
|
568
|
+
const bashProxy = new HostBashProxy(() => {});
|
|
569
|
+
conversation.setHostBrowserProxy(browserProxy);
|
|
570
|
+
conversation.setHostBashProxy(bashProxy);
|
|
571
|
+
|
|
572
|
+
// Mark as having a connected client (interactive desktop path).
|
|
573
|
+
conversation.updateClient(() => {}, false);
|
|
574
|
+
expect(browserProxy.isAvailable()).toBe(true);
|
|
575
|
+
expect(bashProxy.isAvailable()).toBe(true);
|
|
576
|
+
|
|
577
|
+
// The drain queue clears all proxies for non-interactive turns.
|
|
578
|
+
conversation.clearProxyAvailability();
|
|
579
|
+
expect(browserProxy.isAvailable()).toBe(false);
|
|
580
|
+
expect(bashProxy.isAvailable()).toBe(false);
|
|
581
|
+
|
|
582
|
+
// restoreBrowserProxyAvailability should bring back ONLY the browser proxy.
|
|
583
|
+
conversation.restoreBrowserProxyAvailability();
|
|
584
|
+
expect(browserProxy.isAvailable()).toBe(true);
|
|
585
|
+
expect(bashProxy.isAvailable()).toBe(false);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("re-enables the browser proxy even when hasNoClient is true (chrome-extension)", () => {
|
|
589
|
+
// Regression: chrome-extension is non-interactive (hasNoClient stays
|
|
590
|
+
// true so host_bash/host_file tools remain gated), but we still need
|
|
591
|
+
// to provision the hostBrowserProxy so it can service CDP commands.
|
|
592
|
+
// The helper must NOT gate on hasNoClient.
|
|
593
|
+
const conversation = makeConversation();
|
|
594
|
+
const browserProxy = new HostBrowserProxy(() => {});
|
|
595
|
+
conversation.setHostBrowserProxy(browserProxy);
|
|
596
|
+
|
|
597
|
+
// updateClient with hasNoClient=true emulates the non-interactive
|
|
598
|
+
// chrome-extension turn. Host proxies start disabled because
|
|
599
|
+
// updateClient propagates hasNoClient through to updateSender.
|
|
600
|
+
conversation.updateClient(() => {}, true);
|
|
601
|
+
expect(browserProxy.isAvailable()).toBe(false);
|
|
602
|
+
expect(conversation["hasNoClient"]).toBe(true);
|
|
603
|
+
|
|
604
|
+
// The targeted helper bypasses the hasNoClient gate so the
|
|
605
|
+
// single-capability chrome-extension turn can drive the browser
|
|
606
|
+
// via CDP without flipping hasNoClient (which would also enable
|
|
607
|
+
// host_bash/host_file gating downstream).
|
|
608
|
+
conversation.restoreBrowserProxyAvailability();
|
|
609
|
+
expect(browserProxy.isAvailable()).toBe(true);
|
|
610
|
+
// hasNoClient itself MUST remain true so that
|
|
611
|
+
// isToolActiveForContext keeps host_bash/host_file/host_cu gated.
|
|
612
|
+
expect(conversation["hasNoClient"]).toBe(true);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
test("leaves bash/file/cu proxies disabled when called for chrome-extension", () => {
|
|
616
|
+
// Regression: the targeted helper must not accidentally re-enable
|
|
617
|
+
// proxies other than host_browser, even when called from a path that
|
|
618
|
+
// owns multiple proxies (e.g. macOS holdover state with hasNoClient
|
|
619
|
+
// forced true for an explicit non-interactive run).
|
|
620
|
+
const conversation = makeConversation();
|
|
621
|
+
const browserProxy = new HostBrowserProxy(() => {});
|
|
622
|
+
const bashProxy = new HostBashProxy(() => {});
|
|
623
|
+
conversation.setHostBrowserProxy(browserProxy);
|
|
624
|
+
conversation.setHostBashProxy(bashProxy);
|
|
625
|
+
|
|
626
|
+
conversation.updateClient(() => {}, true);
|
|
627
|
+
expect(browserProxy.isAvailable()).toBe(false);
|
|
628
|
+
expect(bashProxy.isAvailable()).toBe(false);
|
|
629
|
+
|
|
630
|
+
conversation.restoreBrowserProxyAvailability();
|
|
631
|
+
expect(browserProxy.isAvailable()).toBe(true);
|
|
632
|
+
// Crucial: bash proxy stays disabled. The helper must touch ONLY the
|
|
633
|
+
// browser proxy.
|
|
634
|
+
expect(bashProxy.isAvailable()).toBe(false);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("uses hostBrowserSenderOverride when set so drain-queue restores preserve the registry-routed sender", () => {
|
|
638
|
+
// Regression (PR #24129 cycle 2): the queue-drain path calls
|
|
639
|
+
// `restoreBrowserProxyAvailability()` on dequeue, which used to pass
|
|
640
|
+
// `this.sendToClient` (the SSE hub emitter) to the proxy, clobbering the
|
|
641
|
+
// chrome-extension registry-routed sender established by the POST
|
|
642
|
+
// /messages handler. The override field lets the HTTP handler pin the
|
|
643
|
+
// registry-routed sender so the drain path preserves it.
|
|
644
|
+
const sseHub: ServerMessage[] = [];
|
|
645
|
+
const registry: ServerMessage[] = [];
|
|
646
|
+
const conversation = makeConversation((msg) => sseHub.push(msg));
|
|
647
|
+
const browserProxy = new HostBrowserProxy(() => {});
|
|
648
|
+
conversation.setHostBrowserProxy(browserProxy);
|
|
649
|
+
|
|
650
|
+
// Simulate updateClient setting sendToClient to the SSE hub and
|
|
651
|
+
// marking the conversation as client-less (chrome-extension is
|
|
652
|
+
// non-interactive).
|
|
653
|
+
conversation.updateClient((msg) => sseHub.push(msg), true);
|
|
654
|
+
expect(browserProxy.isAvailable()).toBe(false);
|
|
655
|
+
|
|
656
|
+
// The HTTP handler stashes the registry-routed sender as the override.
|
|
657
|
+
const registrySender = (msg: ServerMessage) => registry.push(msg);
|
|
658
|
+
conversation.hostBrowserSenderOverride = registrySender;
|
|
659
|
+
|
|
660
|
+
// Drain-queue path calls restoreBrowserProxyAvailability — it must now
|
|
661
|
+
// prefer the override over sendToClient.
|
|
662
|
+
conversation.restoreBrowserProxyAvailability();
|
|
663
|
+
expect(browserProxy.isAvailable()).toBe(true);
|
|
664
|
+
|
|
665
|
+
// Send a frame through the proxy and verify it flows through the
|
|
666
|
+
// registry sender, not the SSE hub.
|
|
667
|
+
const internalSend = (
|
|
668
|
+
browserProxy as unknown as {
|
|
669
|
+
sendToClient: (msg: ServerMessage) => void;
|
|
670
|
+
}
|
|
671
|
+
).sendToClient;
|
|
672
|
+
const probe: ServerMessage = {
|
|
673
|
+
type: "host_browser_cancel",
|
|
674
|
+
requestId: "probe-1",
|
|
675
|
+
} as ServerMessage;
|
|
676
|
+
internalSend(probe);
|
|
677
|
+
expect(registry).toHaveLength(1);
|
|
678
|
+
expect(sseHub.some((m) => m === probe)).toBe(false);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("falls back to sendToClient when hostBrowserSenderOverride is cleared", () => {
|
|
682
|
+
// When a non-chrome-extension turn takes over, the HTTP handler clears
|
|
683
|
+
// the override and restoreBrowserProxyAvailability must fall back to
|
|
684
|
+
// sendToClient (the SSE hub), otherwise macOS turns would route their
|
|
685
|
+
// host_browser frames through the stale chrome-extension registry.
|
|
686
|
+
const sseHub: ServerMessage[] = [];
|
|
687
|
+
const conversation = makeConversation((msg) => sseHub.push(msg));
|
|
688
|
+
const browserProxy = new HostBrowserProxy(() => {});
|
|
689
|
+
conversation.setHostBrowserProxy(browserProxy);
|
|
690
|
+
|
|
691
|
+
// First the chrome-extension path pins the override.
|
|
692
|
+
const registry: ServerMessage[] = [];
|
|
693
|
+
conversation.hostBrowserSenderOverride = (msg) => registry.push(msg);
|
|
694
|
+
conversation.updateClient((msg) => sseHub.push(msg), true);
|
|
695
|
+
conversation.restoreBrowserProxyAvailability();
|
|
696
|
+
|
|
697
|
+
// Then a macOS handoff clears the override.
|
|
698
|
+
conversation.hostBrowserSenderOverride = undefined;
|
|
699
|
+
conversation.updateClient((msg) => sseHub.push(msg), false);
|
|
700
|
+
conversation.restoreBrowserProxyAvailability();
|
|
701
|
+
|
|
702
|
+
const internalSend = (
|
|
703
|
+
browserProxy as unknown as {
|
|
704
|
+
sendToClient: (msg: ServerMessage) => void;
|
|
705
|
+
}
|
|
706
|
+
).sendToClient;
|
|
707
|
+
const probe: ServerMessage = {
|
|
708
|
+
type: "host_browser_cancel",
|
|
709
|
+
requestId: "probe-2",
|
|
710
|
+
} as ServerMessage;
|
|
711
|
+
internalSend(probe);
|
|
712
|
+
expect(sseHub).toContain(probe);
|
|
713
|
+
expect(registry).not.toContain(probe);
|
|
714
|
+
});
|
|
715
|
+
});
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
addMessage,
|
|
36
36
|
createConversation,
|
|
37
37
|
forkConversation,
|
|
38
|
+
getConversationHostAccess,
|
|
38
39
|
getMessages,
|
|
39
40
|
PRIVATE_CONVERSATION_FORK_ERROR,
|
|
40
41
|
} from "../memory/conversation-crud.js";
|
|
@@ -138,6 +139,22 @@ describe("forkConversation", () => {
|
|
|
138
139
|
).toBe(true);
|
|
139
140
|
});
|
|
140
141
|
|
|
142
|
+
test("forked conversations start with host access disabled", async () => {
|
|
143
|
+
const source = createConversation({
|
|
144
|
+
title: "Computer-enabled thread",
|
|
145
|
+
hostAccess: true,
|
|
146
|
+
});
|
|
147
|
+
await addMessage(source.id, "user", "Use the computer", undefined, {
|
|
148
|
+
skipIndexing: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const fork = forkConversation({ conversationId: source.id });
|
|
152
|
+
|
|
153
|
+
expect(getConversationHostAccess(source.id)).toBe(true);
|
|
154
|
+
expect(getConversationHostAccess(fork.id)).toBe(false);
|
|
155
|
+
expect(fork.hostAccess).toBe(0);
|
|
156
|
+
});
|
|
157
|
+
|
|
141
158
|
test("preserves source order when source messages share a timestamp", () => {
|
|
142
159
|
const source = createConversation("Equal timestamp thread");
|
|
143
160
|
const db = getDb();
|
|
@@ -747,6 +747,7 @@ describe("web_search_tool_result structural guard", () => {
|
|
|
747
747
|
// web_search_tool_result has a structurally different content format
|
|
748
748
|
// (array of web_search_result objects) and is not truncated this way.
|
|
749
749
|
"context/tool-result-truncation.ts",
|
|
750
|
+
"context/post-turn-tool-result-truncation.ts",
|
|
750
751
|
|
|
751
752
|
// Anthropic provider type guards define API-specific discriminants.
|
|
752
753
|
// It has a separate isWebSearchToolResultBlock for the other type.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
let authDisabled = true;
|
|
4
|
+
let boundGuardianPrincipalId: string | null = null;
|
|
5
|
+
|
|
6
|
+
mock.module("../util/logger.js", () => ({
|
|
7
|
+
getLogger: () =>
|
|
8
|
+
new Proxy({} as Record<string, unknown>, {
|
|
9
|
+
get: () => () => {},
|
|
10
|
+
}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
mock.module("../config/env.js", () => ({
|
|
14
|
+
isHttpAuthDisabled: () => authDisabled,
|
|
15
|
+
hasUngatedHttpAuthDisabled: () => false,
|
|
16
|
+
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
17
|
+
getGatewayPort: () => 7830,
|
|
18
|
+
getRuntimeHttpPort: () => 7821,
|
|
19
|
+
getRuntimeHttpHost: () => "127.0.0.1",
|
|
20
|
+
getRuntimeGatewayOriginSecret: () => undefined,
|
|
21
|
+
getIngressPublicBaseUrl: () => undefined,
|
|
22
|
+
setIngressPublicBaseUrl: () => {},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
mock.module("../contacts/contact-store.js", () => ({
|
|
26
|
+
findGuardianForChannel: () =>
|
|
27
|
+
boundGuardianPrincipalId
|
|
28
|
+
? {
|
|
29
|
+
channel: { externalUserId: undefined },
|
|
30
|
+
contact: { principalId: boundGuardianPrincipalId },
|
|
31
|
+
}
|
|
32
|
+
: null,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
mock.module("../config/loader.js", () => ({
|
|
36
|
+
getConfig: () => ({
|
|
37
|
+
ui: {},
|
|
38
|
+
model: "test",
|
|
39
|
+
provider: "test",
|
|
40
|
+
memory: { enabled: false },
|
|
41
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
42
|
+
secretDetection: { enabled: false },
|
|
43
|
+
}),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
createConversation,
|
|
48
|
+
updateConversationHostAccess,
|
|
49
|
+
} from "../memory/conversation-crud.js";
|
|
50
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
51
|
+
import { assistantEventHub } from "../runtime/assistant-event-hub.js";
|
|
52
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../runtime/assistant-scope.js";
|
|
53
|
+
import { RuntimeHttpServer } from "../runtime/http-server.js";
|
|
54
|
+
import { conversationManagementRouteDefinitions } from "../runtime/routes/conversation-management-routes.js";
|
|
55
|
+
|
|
56
|
+
initializeDb();
|
|
57
|
+
|
|
58
|
+
const routes = conversationManagementRouteDefinitions({
|
|
59
|
+
switchConversation: async (conversationId) => {
|
|
60
|
+
const conversation = {
|
|
61
|
+
conversationId,
|
|
62
|
+
title: "Switched conversation",
|
|
63
|
+
conversationType: "standard",
|
|
64
|
+
hostAccess: false,
|
|
65
|
+
};
|
|
66
|
+
return conversation;
|
|
67
|
+
},
|
|
68
|
+
renameConversation: () => true,
|
|
69
|
+
clearAllConversations: () => 0,
|
|
70
|
+
cancelGeneration: () => true,
|
|
71
|
+
destroyConversation: () => {},
|
|
72
|
+
undoLastMessage: async () => null,
|
|
73
|
+
regenerateResponse: async () => null,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function findRoute(method: string, endpoint: string) {
|
|
77
|
+
const route = routes.find(
|
|
78
|
+
(routeDef) => routeDef.method === method && routeDef.endpoint === endpoint,
|
|
79
|
+
);
|
|
80
|
+
if (!route) {
|
|
81
|
+
throw new Error(`Route not found: ${method} ${endpoint}`);
|
|
82
|
+
}
|
|
83
|
+
return route;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clearTables(): void {
|
|
87
|
+
const db = getDb();
|
|
88
|
+
db.run("DELETE FROM conversation_assistant_attention_state");
|
|
89
|
+
db.run("DELETE FROM external_conversation_bindings");
|
|
90
|
+
db.run("DELETE FROM conversation_keys");
|
|
91
|
+
db.run("DELETE FROM messages");
|
|
92
|
+
db.run("DELETE FROM conversations");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("conversation host-access transport", () => {
|
|
96
|
+
let server: RuntimeHttpServer | null = null;
|
|
97
|
+
|
|
98
|
+
beforeEach(async () => {
|
|
99
|
+
await server?.stop();
|
|
100
|
+
server = null;
|
|
101
|
+
authDisabled = true;
|
|
102
|
+
boundGuardianPrincipalId = null;
|
|
103
|
+
clearTables();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterAll(async () => {
|
|
107
|
+
await server?.stop();
|
|
108
|
+
resetDb();
|
|
109
|
+
mock.restore();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("GET and PATCH /v1/conversations/:id/host-access use the conversation store", async () => {
|
|
113
|
+
const conversation = createConversation("Host access test");
|
|
114
|
+
|
|
115
|
+
const getRoute = findRoute("GET", "conversations/:id/host-access");
|
|
116
|
+
const getResponse = await getRoute.handler({
|
|
117
|
+
req: new Request("http://localhost/v1/conversations/test/host-access"),
|
|
118
|
+
url: new URL("http://localhost/v1/conversations/test/host-access"),
|
|
119
|
+
server: null as never,
|
|
120
|
+
authContext: {} as never,
|
|
121
|
+
params: { id: conversation.id },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(getResponse.status).toBe(200);
|
|
125
|
+
expect(await getResponse.json()).toEqual({
|
|
126
|
+
conversationId: conversation.id,
|
|
127
|
+
hostAccess: false,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const received: Array<{
|
|
131
|
+
assistantId: string;
|
|
132
|
+
type: string;
|
|
133
|
+
conversationId?: string;
|
|
134
|
+
hostAccess?: boolean;
|
|
135
|
+
}> = [];
|
|
136
|
+
const subscription = assistantEventHub.subscribe(
|
|
137
|
+
{ assistantId: DAEMON_INTERNAL_ASSISTANT_ID },
|
|
138
|
+
(event) => {
|
|
139
|
+
received.push({
|
|
140
|
+
assistantId: event.assistantId,
|
|
141
|
+
type: event.message.type,
|
|
142
|
+
conversationId: event.conversationId,
|
|
143
|
+
hostAccess:
|
|
144
|
+
event.message.type === "conversation_host_access_updated"
|
|
145
|
+
? event.message.hostAccess
|
|
146
|
+
: undefined,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const patchRoute = findRoute("PATCH", "conversations/:id/host-access");
|
|
152
|
+
const patchResponse = await patchRoute.handler({
|
|
153
|
+
req: new Request("http://localhost/v1/conversations/test/host-access", {
|
|
154
|
+
method: "PATCH",
|
|
155
|
+
headers: { "Content-Type": "application/json" },
|
|
156
|
+
body: JSON.stringify({ hostAccess: true }),
|
|
157
|
+
}),
|
|
158
|
+
url: new URL("http://localhost/v1/conversations/test/host-access"),
|
|
159
|
+
server: null as never,
|
|
160
|
+
authContext: {} as never,
|
|
161
|
+
params: { id: conversation.id },
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await Promise.resolve();
|
|
165
|
+
|
|
166
|
+
expect(patchResponse.status).toBe(200);
|
|
167
|
+
expect(await patchResponse.json()).toEqual({
|
|
168
|
+
conversationId: conversation.id,
|
|
169
|
+
hostAccess: true,
|
|
170
|
+
});
|
|
171
|
+
expect(received).toHaveLength(1);
|
|
172
|
+
expect(received[0]).toEqual({
|
|
173
|
+
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
174
|
+
type: "conversation_host_access_updated",
|
|
175
|
+
conversationId: conversation.id,
|
|
176
|
+
hostAccess: true,
|
|
177
|
+
});
|
|
178
|
+
subscription.dispose();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("conversation summaries include hostAccess", async () => {
|
|
182
|
+
const conversation = createConversation("Summary host access");
|
|
183
|
+
updateConversationHostAccess(conversation.id, true);
|
|
184
|
+
|
|
185
|
+
server = new RuntimeHttpServer({
|
|
186
|
+
port: 0,
|
|
187
|
+
bearerToken: "test-bearer-token",
|
|
188
|
+
});
|
|
189
|
+
await server.start();
|
|
190
|
+
|
|
191
|
+
const response = await fetch(
|
|
192
|
+
`http://127.0.0.1:${server.actualPort}/v1/conversations/${conversation.id}`,
|
|
193
|
+
);
|
|
194
|
+
expect(response.status).toBe(200);
|
|
195
|
+
|
|
196
|
+
const body = (await response.json()) as {
|
|
197
|
+
conversation: {
|
|
198
|
+
id: string;
|
|
199
|
+
hostAccess: boolean;
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
expect(body.conversation.id).toBe(conversation.id);
|
|
204
|
+
expect(body.conversation.hostAccess).toBe(true);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("PATCH /v1/conversations/:id/host-access rejects non-guardian actors when auth is enforced", async () => {
|
|
208
|
+
authDisabled = false;
|
|
209
|
+
boundGuardianPrincipalId = "guardian-principal";
|
|
210
|
+
const conversation = createConversation("Guarded host access");
|
|
211
|
+
|
|
212
|
+
const patchRoute = findRoute("PATCH", "conversations/:id/host-access");
|
|
213
|
+
const patchResponse = await patchRoute.handler({
|
|
214
|
+
req: new Request("http://localhost/v1/conversations/test/host-access", {
|
|
215
|
+
method: "PATCH",
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
body: JSON.stringify({ hostAccess: true }),
|
|
218
|
+
}),
|
|
219
|
+
url: new URL("http://localhost/v1/conversations/test/host-access"),
|
|
220
|
+
server: null as never,
|
|
221
|
+
authContext: {
|
|
222
|
+
actorPrincipalId: "trusted-contact-principal",
|
|
223
|
+
} as never,
|
|
224
|
+
params: { id: conversation.id },
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(patchResponse.status).toBe(403);
|
|
228
|
+
});
|
|
229
|
+
});
|