@vellumai/assistant 0.8.10 → 0.8.11-staging.1
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 +62 -1
- package/docs/workspace-tools.md +196 -0
- package/examples/plugins/echo/README.md +3 -3
- package/knip.json +1 -0
- package/openapi.yaml +460 -128
- package/package.json +2 -1
- package/scripts/build-plugin-api.ts +299 -0
- package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
- package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
- package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
- package/src/__tests__/agent-loop-thinking.test.ts +4 -0
- package/src/__tests__/agent-loop.test.ts +578 -5
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
- package/src/__tests__/approval-cascade.test.ts +1 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
- package/src/__tests__/btw-routes.test.ts +0 -1
- package/src/__tests__/build-persisted-content.test.ts +75 -1
- package/src/__tests__/catalog-install-normalize.test.ts +141 -0
- package/src/__tests__/ces-startup-timeout.test.ts +60 -0
- package/src/__tests__/compaction-events.test.ts +1 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
- package/src/__tests__/context-overflow-reducer.test.ts +264 -124
- package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
- package/src/__tests__/conversation-agent-loop.test.ts +131 -551
- package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
- package/src/__tests__/conversation-fork-crud.test.ts +259 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-lifecycle.test.ts +257 -1
- package/src/__tests__/conversation-process-callsite.test.ts +1 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
- package/src/__tests__/conversation-queue.test.ts +1 -39
- package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
- package/src/__tests__/conversation-skill-tools.test.ts +491 -5
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
- package/src/__tests__/conversation-speed-override.test.ts +1 -0
- package/src/__tests__/conversation-store.test.ts +74 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
- package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
- package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
- package/src/__tests__/corrected-target.test.ts +93 -0
- package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
- package/src/__tests__/credential-execution-tools.test.ts +23 -11
- package/src/__tests__/credential-security-invariants.test.ts +6 -1
- package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
- package/src/__tests__/device-id.test.ts +70 -1
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
- package/src/__tests__/empty-response-hook.test.ts +242 -66
- package/src/__tests__/external-plugin-loader.test.ts +0 -31
- package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
- package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
- package/src/__tests__/history-repair-hook.test.ts +228 -3
- package/src/__tests__/host-app-control-proxy.test.ts +45 -0
- package/src/__tests__/host-browser-proxy.test.ts +254 -9
- package/src/__tests__/identity-routes.test.ts +1 -0
- package/src/__tests__/image-recovery-hook.test.ts +387 -0
- package/src/__tests__/injector-chain.test.ts +5 -4
- package/src/__tests__/injector-v3-suppression.test.ts +373 -47
- package/src/__tests__/intent-routing.test.ts +7 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
- package/src/__tests__/notification-decision-strategy.test.ts +3 -3
- package/src/__tests__/oauth-store.test.ts +0 -85
- package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
- package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
- package/src/__tests__/persist-unsendable-image.test.ts +4 -4
- package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
- package/src/__tests__/plugin-bootstrap.test.ts +82 -73
- package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
- package/src/__tests__/plugin-types.test.ts +0 -8
- package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
- package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
- package/src/__tests__/registry.test.ts +240 -1
- package/src/__tests__/require-fresh-approval.test.ts +3 -0
- package/src/__tests__/schedule-routes.test.ts +116 -1
- package/src/__tests__/schedule-store.test.ts +28 -0
- package/src/__tests__/schedule-tools.test.ts +94 -1
- package/src/__tests__/server-history-render.test.ts +39 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
- package/src/__tests__/skills.test.ts +202 -0
- package/src/__tests__/slim-skill-category.test.ts +195 -0
- package/src/__tests__/strip-memory-injections.test.ts +33 -39
- package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
- package/src/__tests__/title-generate-hook.test.ts +9 -7
- package/src/__tests__/tool-audit-listener.test.ts +264 -1
- package/src/__tests__/tool-error-hook.test.ts +4 -3
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/tool-start-timestamp.test.ts +218 -0
- package/src/__tests__/tools-get-route.test.ts +202 -0
- package/src/__tests__/workspace-tool-loader.test.ts +319 -0
- package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
- package/src/agent/loop.ts +569 -319
- package/src/api/events/tool-result.ts +9 -0
- package/src/api/events/tool-use-start.ts +7 -0
- package/src/api/index.ts +10 -0
- package/src/api/responses/conversation-message.ts +135 -27
- package/src/api/responses/memory-v3-selection-log.ts +4 -4
- package/src/approvals/guardian-request-resolvers.ts +26 -0
- package/src/browser-session/backends/host-bridge.ts +29 -0
- package/src/browser-session/index.ts +1 -0
- package/src/browser-session/types.ts +5 -1
- package/src/cli/commands/__tests__/schedules.test.ts +62 -4
- package/src/cli/commands/__tests__/skills.test.ts +53 -0
- package/src/cli/commands/channel-verification-sessions.ts +6 -6
- package/src/cli/commands/inference-providers.ts +0 -8
- package/src/cli/commands/plugins.ts +2 -2
- package/src/cli/commands/schedules.ts +27 -4
- package/src/cli/commands/skills.ts +187 -146
- package/src/cli/commands/tools.ts +106 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
- package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
- package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
- package/src/cli/lib/install-from-github.ts +187 -117
- package/src/cli/lib/plugin-catalog-cache.ts +9 -9
- package/src/cli/lib/plugin-details.ts +38 -68
- package/src/cli/lib/plugin-marketplace.ts +42 -14
- package/src/cli/lib/search-plugins.ts +29 -129
- package/src/cli/program.ts +2 -0
- package/src/config/bundled-skills/acp/SKILL.md +1 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
- package/src/config/bundled-skills/app-control/SKILL.md +1 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
- package/src/config/bundled-skills/contacts/SKILL.md +1 -0
- package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
- package/src/config/bundled-skills/followups/SKILL.md +1 -0
- package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
- package/src/config/bundled-skills/messaging/SKILL.md +1 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
- package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/SKILL.md +1 -0
- package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
- package/src/config/bundled-skills/sequences/SKILL.md +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +1 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
- package/src/config/bundled-skills/subagent/SKILL.md +1 -0
- package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
- package/src/config/env-registry.ts +23 -0
- package/src/config/feature-flag-registry.json +17 -81
- package/src/config/loader.ts +5 -22
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
- package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
- package/src/config/schemas/compaction-logs.ts +79 -0
- package/src/config/schemas/memory-v2.ts +0 -8
- package/src/config/schemas/memory-v3.ts +104 -33
- package/src/config/seed-inference-profiles.ts +1 -1
- package/src/config/skills.ts +117 -47
- package/src/context/compactor.ts +11 -0
- package/src/context/strip-injections.ts +38 -4
- package/src/credential-execution/feature-gates.ts +0 -21
- package/src/credential-execution/startup-timeout.ts +32 -4
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
- package/src/daemon/conversation-agent-loop.ts +123 -658
- package/src/daemon/conversation-error.ts +6 -33
- package/src/daemon/conversation-lifecycle.ts +1 -1
- package/src/daemon/conversation-runtime-assembly.ts +183 -22
- package/src/daemon/conversation-skill-tools.ts +137 -8
- package/src/daemon/conversation-slash.ts +0 -14
- package/src/daemon/conversation-store.ts +2 -19
- package/src/daemon/conversation-tool-setup.ts +87 -1
- package/src/daemon/conversation.ts +119 -50
- package/src/daemon/external-plugins-bootstrap.ts +36 -95
- package/src/daemon/handlers/config-channels.ts +11 -2
- package/src/daemon/handlers/shared.ts +9 -1
- package/src/daemon/handlers/skills.ts +10 -4
- package/src/daemon/host-app-control-proxy.ts +72 -57
- package/src/daemon/host-browser-proxy.ts +117 -22
- package/src/daemon/lifecycle.ts +34 -5
- package/src/daemon/message-protocol.ts +0 -7
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/skills.ts +17 -0
- package/src/daemon/providers-setup.ts +3 -0
- package/src/daemon/server.ts +3 -3
- package/src/daemon/tool-setup-types.ts +9 -3
- package/src/daemon/trust-context.ts +23 -0
- package/src/daemon/workspace-tools-watcher.ts +324 -0
- package/src/events/tool-audit-listener.ts +78 -16
- package/src/events/tool-metrics-listener.ts +2 -5
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
- package/src/memory/__tests__/conversation-queries.test.ts +176 -0
- package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
- package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
- package/src/memory/conversation-crud.ts +134 -3
- package/src/memory/conversation-queries.ts +64 -7
- package/src/memory/db-init.ts +14 -0
- package/src/memory/embedding-backend.test.ts +130 -1
- package/src/memory/embedding-backend.ts +79 -106
- package/src/memory/embedding-gemini.ts +5 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
- package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
- package/src/memory/graph/conversation-graph-memory.ts +36 -25
- package/src/memory/graph/tool-handlers.ts +3 -0
- package/src/memory/job-handlers/cleanup.ts +3 -1
- package/src/memory/jobs-store.ts +0 -28
- package/src/memory/jobs-worker.ts +10 -22
- package/src/memory/memory-marker.ts +29 -0
- package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
- package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
- package/src/memory/migrations/270-schedule-description.ts +36 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
- package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
- package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
- package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
- package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
- package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
- package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
- package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
- package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
- package/src/memory/migrations/index.ts +10 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +16 -0
- package/src/memory/schema/infrastructure.ts +26 -0
- package/src/memory/skill-loaded-events-store.test.ts +160 -0
- package/src/memory/skill-loaded-events-store.ts +95 -0
- package/src/memory/tool-executed-events-store.test.ts +219 -0
- package/src/memory/tool-executed-events-store.ts +102 -0
- package/src/memory/tool-usage-store.ts +15 -3
- package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
- package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
- package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
- package/src/memory/v2/__tests__/page-store.test.ts +33 -0
- package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
- package/src/memory/v2/activation-store.ts +50 -1
- package/src/memory/v2/consolidation-job.ts +113 -29
- package/src/memory/v2/injected-block-slugs.ts +79 -0
- package/src/memory/v2/injection.ts +6 -1
- package/src/memory/v2/prompts/consolidation.ts +414 -13
- package/src/memory/v2/router.ts +2 -28
- package/src/memory/v2/static-context.ts +1 -1
- package/src/memory/v2/types.ts +16 -0
- package/src/notifications/__tests__/copy-composer.test.ts +244 -0
- package/src/notifications/access-request-copy.ts +298 -0
- package/src/notifications/adapters/slack.ts +3 -3
- package/src/notifications/adapters/telegram.ts +2 -1
- package/src/notifications/copy-composer.ts +49 -267
- package/src/notifications/decision-engine.ts +16 -35
- package/src/notifications/home-feed-side-effect.ts +1 -6
- package/src/oauth/oauth-store.ts +0 -9
- package/src/permissions/checker.test.ts +83 -1
- package/src/permissions/checker.ts +25 -2
- package/src/permissions/gateway-threshold-reader.test.ts +182 -0
- package/src/permissions/gateway-threshold-reader.ts +80 -0
- package/src/platform/client.ts +1 -3
- package/src/platform/feature-gate.ts +3 -12
- package/src/plugin-api/constants.ts +4 -2
- package/src/plugin-api/index.ts +63 -11
- package/src/plugin-api/types.ts +236 -71
- package/src/plugins/defaults/compaction/compact.ts +66 -2
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
- package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
- package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
- package/src/plugins/defaults/compaction/window-manager.ts +303 -1
- package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
- package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
- package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
- package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
- package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
- package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
- package/src/plugins/defaults/history-repair/terminal.ts +39 -2
- package/src/plugins/defaults/image-recovery/detect.ts +25 -0
- package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
- package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
- package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
- package/src/plugins/defaults/image-recovery/package.json +14 -0
- package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
- package/src/plugins/defaults/index.ts +71 -5
- package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
- package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
- package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
- package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
- package/src/plugins/defaults/memory-retrieval/package.json +14 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
- package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
- package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
- package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
- package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
- package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
- package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
- package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
- package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
- package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
- package/src/plugins/pipeline.ts +8 -5
- package/src/plugins/types.ts +5 -51
- package/src/providers/cache-control.ts +26 -0
- package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
- package/src/providers/model-catalog.ts +13 -1
- package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
- package/src/providers/openai/chat-completions-provider.ts +46 -0
- package/src/providers/openai/responses-provider.ts +45 -0
- package/src/providers/registry.ts +0 -8
- package/src/runtime/__tests__/agent-wake.test.ts +0 -1
- package/src/runtime/agent-wake.ts +13 -0
- package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
- package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
- package/src/runtime/routes/acp-routes.test.ts +106 -0
- package/src/runtime/routes/acp-routes.ts +248 -2
- package/src/runtime/routes/browser-tabs-routes.ts +1 -1
- package/src/runtime/routes/channel-verification-routes.ts +14 -5
- package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
- package/src/runtime/routes/consolidation-routes.ts +6 -82
- package/src/runtime/routes/conversation-list-routes.ts +6 -0
- package/src/runtime/routes/conversation-management-routes.ts +71 -0
- package/src/runtime/routes/identity-routes.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
- package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
- package/src/runtime/routes/plugins-routes.ts +34 -45
- package/src/runtime/routes/schedule-routes.ts +43 -5
- package/src/runtime/routes/settings-routes.ts +140 -15
- package/src/runtime/routes/skills-routes.ts +18 -6
- package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
- package/src/runtime/services/conversation-serializer.ts +38 -1
- package/src/runtime/verification-outbound-actions.ts +147 -2
- package/src/runtime/verification-templates.ts +29 -3
- package/src/schedule/schedule-store.ts +19 -0
- package/src/skills/catalog-install.ts +77 -13
- package/src/tasks/task-scheduler.ts +1 -0
- package/src/telemetry/types.ts +66 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
- package/src/telemetry/usage-telemetry-reporter.ts +213 -20
- package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
- package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
- package/src/tools/browser/browser-execution.ts +27 -9
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
- package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
- package/src/tools/browser/cdp-client/factory.ts +217 -17
- package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
- package/src/tools/browser/cdp-client/types.ts +22 -2
- package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
- package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
- package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
- package/src/tools/executor.ts +39 -7
- package/src/tools/registry.ts +387 -5
- package/src/tools/schedule/create.ts +16 -0
- package/src/tools/schedule/list.ts +12 -4
- package/src/tools/schedule/update.ts +12 -0
- package/src/tools/skills/load.ts +11 -6
- package/src/tools/terminal/safe-env.ts +2 -0
- package/src/tools/types.ts +65 -9
- package/src/tools/workspace-tools/loader.ts +673 -0
- package/src/usage/attribution.ts +28 -0
- package/src/util/device-id.ts +17 -3
- package/src/util/platform.ts +16 -0
- package/tsconfig.plugin-api.json +13 -0
- package/src/__tests__/plugin-external-api.test.ts +0 -68
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
- package/src/daemon/message-types/browser.ts +0 -10
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
- package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
- package/src/plugins/external-api.ts +0 -114
- package/src/plugins/plugin-skill-contributions.ts +0 -292
|
@@ -5,7 +5,7 @@ import type { ToolContext } from "../../../types.js";
|
|
|
5
5
|
import { CdpError } from "../errors.js";
|
|
6
6
|
|
|
7
7
|
type FakeClient = {
|
|
8
|
-
kind: "extension" | "local" | "cdp-inspect";
|
|
8
|
+
kind: "extension" | "local" | "cdp-inspect" | "host-bridge";
|
|
9
9
|
conversationId: string;
|
|
10
10
|
send: ReturnType<typeof mock>;
|
|
11
11
|
dispose: ReturnType<typeof mock>;
|
|
@@ -20,6 +20,15 @@ function makeFakeExtensionClient(conversationId: string): FakeClient {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function makeFakeHostBridgeClient(conversationId: string): FakeClient {
|
|
24
|
+
return {
|
|
25
|
+
kind: "host-bridge",
|
|
26
|
+
conversationId,
|
|
27
|
+
send: mock(async () => ({ ok: true, via: "host-bridge" })),
|
|
28
|
+
dispose: mock(() => {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
function makeFakeLocalClient(conversationId: string): FakeClient {
|
|
24
33
|
return {
|
|
25
34
|
kind: "local",
|
|
@@ -50,6 +59,17 @@ const createExtensionCdpClientMock = mock(
|
|
|
50
59
|
},
|
|
51
60
|
);
|
|
52
61
|
|
|
62
|
+
const createHostBridgeCdpClientMock = mock(
|
|
63
|
+
(
|
|
64
|
+
_proxy: HostBrowserProxy,
|
|
65
|
+
conversationId: string,
|
|
66
|
+
_sourceActorPrincipalId?: string,
|
|
67
|
+
) => {
|
|
68
|
+
const client = makeFakeHostBridgeClient(conversationId);
|
|
69
|
+
return client;
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
53
73
|
const createLocalCdpClientMock = mock((conversationId: string) => {
|
|
54
74
|
const client = makeFakeLocalClient(conversationId);
|
|
55
75
|
lastLocalClient = client;
|
|
@@ -86,12 +106,17 @@ const logDebugCalls: Array<{ args: unknown[] }> = [];
|
|
|
86
106
|
// suite stubs out.
|
|
87
107
|
import * as realCdpInspectClient from "../cdp-inspect-client.js";
|
|
88
108
|
import * as realExtensionCdpClient from "../extension-cdp-client.js";
|
|
109
|
+
import * as realHostBridgeCdpClient from "../host-bridge-cdp-client.js";
|
|
89
110
|
import * as realLocalCdpClient from "../local-cdp-client.js";
|
|
90
111
|
|
|
91
112
|
mock.module("../extension-cdp-client.js", () => ({
|
|
92
113
|
...realExtensionCdpClient,
|
|
93
114
|
createExtensionCdpClient: createExtensionCdpClientMock,
|
|
94
115
|
}));
|
|
116
|
+
mock.module("../host-bridge-cdp-client.js", () => ({
|
|
117
|
+
...realHostBridgeCdpClient,
|
|
118
|
+
createHostBridgeCdpClient: createHostBridgeCdpClientMock,
|
|
119
|
+
}));
|
|
95
120
|
mock.module("../local-cdp-client.js", () => ({
|
|
96
121
|
...realLocalCdpClient,
|
|
97
122
|
createLocalCdpClient: createLocalCdpClientMock,
|
|
@@ -161,6 +186,10 @@ const {
|
|
|
161
186
|
_getDesktopAutoCooldownSince,
|
|
162
187
|
recordDesktopAutoCooldown,
|
|
163
188
|
isDesktopAutoCooldownActive,
|
|
189
|
+
_resetHostBridgeCooldown,
|
|
190
|
+
_getHostBridgeCooldownSince,
|
|
191
|
+
recordHostBridgeCooldown,
|
|
192
|
+
isHostBridgeCooldownActive,
|
|
164
193
|
} = await import("../factory.js");
|
|
165
194
|
|
|
166
195
|
/**
|
|
@@ -210,17 +239,40 @@ function makeUnavailableProxy(): HostBrowserProxy {
|
|
|
210
239
|
} as unknown as HostBrowserProxy;
|
|
211
240
|
}
|
|
212
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Create a fake HostBrowserProxy with actor-scoped availability, mirroring
|
|
244
|
+
* the real implementation: with an actor, only that actor's clients count;
|
|
245
|
+
* without one, any client counts.
|
|
246
|
+
*/
|
|
247
|
+
function makeActorScopedProxy(opts: {
|
|
248
|
+
extensionActors: string[];
|
|
249
|
+
bridgeActors: string[];
|
|
250
|
+
}): HostBrowserProxy {
|
|
251
|
+
const allActors = [...opts.extensionActors, ...opts.bridgeActors];
|
|
252
|
+
return {
|
|
253
|
+
request: mock(async () => ({})),
|
|
254
|
+
isAvailable: (actor?: string) =>
|
|
255
|
+
actor == null ? allActors.length > 0 : allActors.includes(actor),
|
|
256
|
+
hasExtensionClient: (actor?: string) =>
|
|
257
|
+
actor == null
|
|
258
|
+
? opts.extensionActors.length > 0
|
|
259
|
+
: opts.extensionActors.includes(actor),
|
|
260
|
+
} as unknown as HostBrowserProxy;
|
|
261
|
+
}
|
|
262
|
+
|
|
213
263
|
describe("getCdpClient", () => {
|
|
214
264
|
beforeEach(() => {
|
|
215
265
|
createExtensionCdpClientMock.mockClear();
|
|
216
266
|
createLocalCdpClientMock.mockClear();
|
|
217
267
|
createCdpInspectClientMock.mockClear();
|
|
268
|
+
createHostBridgeCdpClientMock.mockClear();
|
|
218
269
|
lastExtensionClient = undefined;
|
|
219
270
|
lastLocalClient = undefined;
|
|
220
271
|
lastCdpInspectClient = undefined;
|
|
221
272
|
cdpInspectEnabled = false;
|
|
222
273
|
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
223
274
|
_resetDesktopAutoCooldown();
|
|
275
|
+
_resetHostBridgeCooldown();
|
|
224
276
|
logWarnCalls.length = 0;
|
|
225
277
|
logDebugCalls.length = 0;
|
|
226
278
|
mockSingletonProxy = null;
|
|
@@ -785,6 +837,7 @@ describe("buildCandidateList", () => {
|
|
|
785
837
|
cdpInspectEnabled = false;
|
|
786
838
|
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
787
839
|
_resetDesktopAutoCooldown();
|
|
840
|
+
_resetHostBridgeCooldown();
|
|
788
841
|
mockSingletonProxy = null;
|
|
789
842
|
});
|
|
790
843
|
|
|
@@ -816,10 +869,11 @@ describe("buildCandidateList", () => {
|
|
|
816
869
|
expect(candidates[0].kind).toBe("local");
|
|
817
870
|
});
|
|
818
871
|
|
|
819
|
-
test("
|
|
872
|
+
test("uses host-bridge candidate when only macOS SSE bridge is connected", () => {
|
|
820
873
|
// isAvailable() = true but hasExtensionClient() = false: only macOS bridge.
|
|
821
|
-
// The
|
|
822
|
-
//
|
|
874
|
+
// The bridge gets its own first-class candidate kind — it must NOT be
|
|
875
|
+
// labelled "extension" (it routes raw CDP through localhost:9222 on the
|
|
876
|
+
// host and cannot serve Vellum.* pseudo-methods).
|
|
823
877
|
const fakeProxy = makeMacosBridgeOnlyProxy();
|
|
824
878
|
mockSingletonProxy = fakeProxy;
|
|
825
879
|
const ctx = makeContext({
|
|
@@ -829,6 +883,180 @@ describe("buildCandidateList", () => {
|
|
|
829
883
|
const candidates = buildCandidateList(ctx);
|
|
830
884
|
|
|
831
885
|
expect(candidates.every((c) => c.kind !== "extension")).toBe(true);
|
|
886
|
+
expect(candidates[0].kind).toBe("host-bridge");
|
|
887
|
+
expect(candidates[candidates.length - 1].kind).toBe("local");
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
test("host-bridge candidate is absent when an extension is connected", () => {
|
|
891
|
+
mockSingletonProxy = makeAvailableProxy();
|
|
892
|
+
const ctx = makeContext({ conversationId: "candidates-ext-no-bridge" });
|
|
893
|
+
|
|
894
|
+
const candidates = buildCandidateList(ctx);
|
|
895
|
+
|
|
896
|
+
expect(candidates[0].kind).toBe("extension");
|
|
897
|
+
expect(candidates.every((c) => c.kind !== "host-bridge")).toBe(true);
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
test("host-bridge candidate is absent when the proxy is unavailable", () => {
|
|
901
|
+
mockSingletonProxy = makeUnavailableProxy();
|
|
902
|
+
const ctx = makeContext({ conversationId: "candidates-no-bridge" });
|
|
903
|
+
|
|
904
|
+
const candidates = buildCandidateList(ctx);
|
|
905
|
+
|
|
906
|
+
expect(candidates.every((c) => c.kind !== "host-bridge")).toBe(true);
|
|
907
|
+
expect(candidates[0].kind).toBe("local");
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
test("host-bridge candidate is skipped while the bridge cooldown is active", () => {
|
|
911
|
+
mockSingletonProxy = makeMacosBridgeOnlyProxy();
|
|
912
|
+
const ctx = makeContext({ conversationId: "candidates-bridge-cooldown" });
|
|
913
|
+
|
|
914
|
+
recordHostBridgeCooldown();
|
|
915
|
+
const candidates = buildCandidateList(ctx);
|
|
916
|
+
|
|
917
|
+
expect(candidates.every((c) => c.kind !== "host-bridge")).toBe(true);
|
|
918
|
+
|
|
919
|
+
_resetHostBridgeCooldown();
|
|
920
|
+
const after = buildCandidateList(ctx);
|
|
921
|
+
expect(after[0].kind).toBe("host-bridge");
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
test("host-bridge cooldown is independent of the desktop-auto cooldown", () => {
|
|
925
|
+
mockSingletonProxy = makeMacosBridgeOnlyProxy();
|
|
926
|
+
const ctx = makeContext({
|
|
927
|
+
conversationId: "candidates-bridge-cooldown-independent",
|
|
928
|
+
transportInterface: "macos",
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
recordDesktopAutoCooldown();
|
|
932
|
+
const candidates = buildCandidateList(ctx);
|
|
933
|
+
|
|
934
|
+
// Desktop-auto cooldown suppresses cdp-inspect but not the bridge.
|
|
935
|
+
expect(candidates[0].kind).toBe("host-bridge");
|
|
936
|
+
expect(candidates.every((c) => c.kind !== "cdp-inspect")).toBe(true);
|
|
937
|
+
expect(isHostBridgeCooldownActive(30_000)).toBe(false);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test("host-bridge cooldown is scoped per actor", () => {
|
|
941
|
+
// One actor's missing debug port must not suppress another actor's
|
|
942
|
+
// only route to their own Chrome on a multi-actor cloud daemon.
|
|
943
|
+
mockSingletonProxy = makeActorScopedProxy({
|
|
944
|
+
extensionActors: [],
|
|
945
|
+
bridgeActors: ["actor-a", "actor-b"],
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
recordHostBridgeCooldown("actor-a");
|
|
949
|
+
|
|
950
|
+
const forA = buildCandidateList(
|
|
951
|
+
makeContext({
|
|
952
|
+
conversationId: "cooldown-actor-a",
|
|
953
|
+
sourceActorPrincipalId: "actor-a",
|
|
954
|
+
}),
|
|
955
|
+
);
|
|
956
|
+
expect(forA.every((c) => c.kind !== "host-bridge")).toBe(true);
|
|
957
|
+
|
|
958
|
+
const forB = buildCandidateList(
|
|
959
|
+
makeContext({
|
|
960
|
+
conversationId: "cooldown-actor-b",
|
|
961
|
+
sourceActorPrincipalId: "actor-b",
|
|
962
|
+
}),
|
|
963
|
+
);
|
|
964
|
+
expect(forB[0].kind).toBe("host-bridge");
|
|
965
|
+
|
|
966
|
+
// Actor-scoped cooldowns do not leak into the no-actor default slot.
|
|
967
|
+
expect(isHostBridgeCooldownActive(30_000)).toBe(false);
|
|
968
|
+
expect(isHostBridgeCooldownActive(30_000, "actor-a")).toBe(true);
|
|
969
|
+
expect(isHostBridgeCooldownActive(30_000, "actor-b")).toBe(false);
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
test("ordering on a macOS turn with bridge only: host-bridge > cdp-inspect > local", () => {
|
|
974
|
+
mockSingletonProxy = makeMacosBridgeOnlyProxy();
|
|
975
|
+
const ctx = makeContext({
|
|
976
|
+
conversationId: "candidates-bridge-order",
|
|
977
|
+
transportInterface: "macos",
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
const candidates = buildCandidateList(ctx);
|
|
981
|
+
|
|
982
|
+
expect(candidates.map((c) => c.kind)).toEqual([
|
|
983
|
+
"host-bridge",
|
|
984
|
+
"cdp-inspect",
|
|
985
|
+
"local",
|
|
986
|
+
]);
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
test("host-bridge create() threads proxy + actor and never reads pinned tabs", () => {
|
|
990
|
+
const fakeProxy = makeMacosBridgeOnlyProxy();
|
|
991
|
+
mockSingletonProxy = fakeProxy;
|
|
992
|
+
const ctx = makeContext({
|
|
993
|
+
conversationId: "candidates-bridge-create",
|
|
994
|
+
sourceActorPrincipalId: "actor-1",
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const candidates = buildCandidateList(ctx);
|
|
998
|
+
expect(candidates[0].kind).toBe("host-bridge");
|
|
999
|
+
candidates[0].create();
|
|
1000
|
+
|
|
1001
|
+
expect(createHostBridgeCdpClientMock).toHaveBeenCalledWith(
|
|
1002
|
+
fakeProxy,
|
|
1003
|
+
"candidates-bridge-create",
|
|
1004
|
+
"actor-1",
|
|
1005
|
+
);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
test("multi-actor: another actor's extension does not select the extension candidate", () => {
|
|
1009
|
+
// Actor A has an extension; actor B has only the bridge. B's
|
|
1010
|
+
// conversation must get the host-bridge candidate, not an
|
|
1011
|
+
// extension-labelled client that the proxy would route to B's bridge.
|
|
1012
|
+
mockSingletonProxy = makeActorScopedProxy({
|
|
1013
|
+
extensionActors: ["actor-a"],
|
|
1014
|
+
bridgeActors: ["actor-b"],
|
|
1015
|
+
});
|
|
1016
|
+
const ctx = makeContext({
|
|
1017
|
+
conversationId: "candidates-multi-actor-b",
|
|
1018
|
+
sourceActorPrincipalId: "actor-b",
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const candidates = buildCandidateList(ctx);
|
|
1022
|
+
|
|
1023
|
+
expect(candidates[0].kind).toBe("host-bridge");
|
|
1024
|
+
expect(candidates.every((c) => c.kind !== "extension")).toBe(true);
|
|
1025
|
+
});
|
|
1026
|
+
|
|
1027
|
+
test("multi-actor: the extension owner still gets the extension candidate", () => {
|
|
1028
|
+
mockSingletonProxy = makeActorScopedProxy({
|
|
1029
|
+
extensionActors: ["actor-a"],
|
|
1030
|
+
bridgeActors: ["actor-b"],
|
|
1031
|
+
});
|
|
1032
|
+
const ctx = makeContext({
|
|
1033
|
+
conversationId: "candidates-multi-actor-a",
|
|
1034
|
+
sourceActorPrincipalId: "actor-a",
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
const candidates = buildCandidateList(ctx);
|
|
1038
|
+
|
|
1039
|
+
expect(candidates[0].kind).toBe("extension");
|
|
1040
|
+
expect(candidates.every((c) => c.kind !== "host-bridge")).toBe(true);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
test("multi-actor: actor with no clients at all gets neither proxy candidate", () => {
|
|
1044
|
+
mockSingletonProxy = makeActorScopedProxy({
|
|
1045
|
+
extensionActors: ["actor-a"],
|
|
1046
|
+
bridgeActors: ["actor-b"],
|
|
1047
|
+
});
|
|
1048
|
+
const ctx = makeContext({
|
|
1049
|
+
conversationId: "candidates-multi-actor-c",
|
|
1050
|
+
sourceActorPrincipalId: "actor-c",
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
const candidates = buildCandidateList(ctx);
|
|
1054
|
+
|
|
1055
|
+
expect(
|
|
1056
|
+
candidates.every(
|
|
1057
|
+
(c) => c.kind !== "extension" && c.kind !== "host-bridge",
|
|
1058
|
+
),
|
|
1059
|
+
).toBe(true);
|
|
832
1060
|
expect(candidates[candidates.length - 1].kind).toBe("local");
|
|
833
1061
|
});
|
|
834
1062
|
|
|
@@ -931,12 +1159,14 @@ describe("buildChainedClient failover", () => {
|
|
|
931
1159
|
createExtensionCdpClientMock.mockClear();
|
|
932
1160
|
createLocalCdpClientMock.mockClear();
|
|
933
1161
|
createCdpInspectClientMock.mockClear();
|
|
1162
|
+
createHostBridgeCdpClientMock.mockClear();
|
|
934
1163
|
lastExtensionClient = undefined;
|
|
935
1164
|
lastLocalClient = undefined;
|
|
936
1165
|
lastCdpInspectClient = undefined;
|
|
937
1166
|
cdpInspectEnabled = false;
|
|
938
1167
|
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
939
1168
|
_resetDesktopAutoCooldown();
|
|
1169
|
+
_resetHostBridgeCooldown();
|
|
940
1170
|
logWarnCalls.length = 0;
|
|
941
1171
|
logDebugCalls.length = 0;
|
|
942
1172
|
mockSingletonProxy = null;
|
|
@@ -1030,6 +1260,94 @@ describe("buildChainedClient failover", () => {
|
|
|
1030
1260
|
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
1031
1261
|
});
|
|
1032
1262
|
|
|
1263
|
+
test("fails over from host-bridge to local and records the bridge cooldown", async () => {
|
|
1264
|
+
mockSingletonProxy = makeMacosBridgeOnlyProxy();
|
|
1265
|
+
|
|
1266
|
+
// Bridge fails like Chrome without a debug port: structured
|
|
1267
|
+
// "unreachable" envelope → transport_error.
|
|
1268
|
+
createHostBridgeCdpClientMock.mockImplementationOnce(
|
|
1269
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
1270
|
+
const c = makeFakeHostBridgeClient(conversationId);
|
|
1271
|
+
c.send = mock(async () => {
|
|
1272
|
+
throw new CdpError(
|
|
1273
|
+
"transport_error",
|
|
1274
|
+
"HTTP 404 from http://localhost:9222/json/list",
|
|
1275
|
+
{ cdpMethod: "Page.navigate" },
|
|
1276
|
+
);
|
|
1277
|
+
});
|
|
1278
|
+
return c;
|
|
1279
|
+
},
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
const ctx = makeContext({ conversationId: "failover-bridge-to-local" });
|
|
1283
|
+
|
|
1284
|
+
const client = getCdpClient(ctx);
|
|
1285
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1286
|
+
"Page.navigate",
|
|
1287
|
+
{ url: "https://example.com" },
|
|
1288
|
+
);
|
|
1289
|
+
|
|
1290
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
1291
|
+
expect(createHostBridgeCdpClientMock).toHaveBeenCalledTimes(1);
|
|
1292
|
+
expect(createLocalCdpClientMock).toHaveBeenCalledTimes(1);
|
|
1293
|
+
// Cooldown was recorded so the next candidate list skips the bridge.
|
|
1294
|
+
expect(_getHostBridgeCooldownSince()).toBeGreaterThan(0);
|
|
1295
|
+
expect(_getDesktopAutoCooldownSince()).toBe(0);
|
|
1296
|
+
const after = buildCandidateList(ctx);
|
|
1297
|
+
expect(after.every((c) => c.kind !== "host-bridge")).toBe(true);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
test("host-bridge failover records the cooldown under the failing actor only", async () => {
|
|
1301
|
+
mockSingletonProxy = makeActorScopedProxy({
|
|
1302
|
+
extensionActors: [],
|
|
1303
|
+
bridgeActors: ["actor-a", "actor-b"],
|
|
1304
|
+
});
|
|
1305
|
+
createHostBridgeCdpClientMock.mockImplementationOnce(
|
|
1306
|
+
(_proxy: HostBrowserProxy, conversationId: string) => {
|
|
1307
|
+
const c = makeFakeHostBridgeClient(conversationId);
|
|
1308
|
+
c.send = mock(async () => {
|
|
1309
|
+
throw new CdpError(
|
|
1310
|
+
"transport_error",
|
|
1311
|
+
"HTTP 404 from http://localhost:9222/json/list",
|
|
1312
|
+
{ cdpMethod: "Page.navigate" },
|
|
1313
|
+
);
|
|
1314
|
+
});
|
|
1315
|
+
return c;
|
|
1316
|
+
},
|
|
1317
|
+
);
|
|
1318
|
+
|
|
1319
|
+
const client = getCdpClient(
|
|
1320
|
+
makeContext({
|
|
1321
|
+
conversationId: "cooldown-record-actor-a",
|
|
1322
|
+
sourceActorPrincipalId: "actor-a",
|
|
1323
|
+
}),
|
|
1324
|
+
);
|
|
1325
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1326
|
+
"Page.navigate",
|
|
1327
|
+
{ url: "https://example.com" },
|
|
1328
|
+
);
|
|
1329
|
+
|
|
1330
|
+
expect(result).toEqual({ ok: true, via: "local" });
|
|
1331
|
+
expect(_getHostBridgeCooldownSince("actor-a")).toBeGreaterThan(0);
|
|
1332
|
+
expect(_getHostBridgeCooldownSince("actor-b")).toBe(0);
|
|
1333
|
+
expect(_getHostBridgeCooldownSince()).toBe(0);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
test("sticky host-bridge client rejects tab operations with a clear error", async () => {
|
|
1337
|
+
mockSingletonProxy = makeMacosBridgeOnlyProxy();
|
|
1338
|
+
const ctx = makeContext({ conversationId: "bridge-tab-ops" });
|
|
1339
|
+
|
|
1340
|
+
const client = getCdpClient(ctx);
|
|
1341
|
+
await client.send("Page.navigate", { url: "https://example.com" });
|
|
1342
|
+
expect(client.kind).toBe("host-bridge");
|
|
1343
|
+
|
|
1344
|
+
// The fake bridge client has no listTabs/selectTab/closeTab, so the
|
|
1345
|
+
// chained client surfaces the not-supported transport_error.
|
|
1346
|
+
await expect(client.listTabs()).rejects.toThrow(
|
|
1347
|
+
"extension backend required",
|
|
1348
|
+
);
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1033
1351
|
test("does NOT fail over on cdp_error -- propagates immediately", async () => {
|
|
1034
1352
|
cdpInspectEnabled = true;
|
|
1035
1353
|
const fakeProxy = makeAvailableProxy();
|
|
@@ -1245,12 +1563,14 @@ describe("desktop-auto cdp-inspect (macOS)", () => {
|
|
|
1245
1563
|
createExtensionCdpClientMock.mockClear();
|
|
1246
1564
|
createLocalCdpClientMock.mockClear();
|
|
1247
1565
|
createCdpInspectClientMock.mockClear();
|
|
1566
|
+
createHostBridgeCdpClientMock.mockClear();
|
|
1248
1567
|
lastExtensionClient = undefined;
|
|
1249
1568
|
lastLocalClient = undefined;
|
|
1250
1569
|
lastCdpInspectClient = undefined;
|
|
1251
1570
|
cdpInspectEnabled = false;
|
|
1252
1571
|
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
1253
1572
|
_resetDesktopAutoCooldown();
|
|
1573
|
+
_resetHostBridgeCooldown();
|
|
1254
1574
|
logWarnCalls.length = 0;
|
|
1255
1575
|
logDebugCalls.length = 0;
|
|
1256
1576
|
mockSingletonProxy = null;
|
|
@@ -1597,12 +1917,14 @@ describe("pinned-mode selection", () => {
|
|
|
1597
1917
|
createExtensionCdpClientMock.mockClear();
|
|
1598
1918
|
createLocalCdpClientMock.mockClear();
|
|
1599
1919
|
createCdpInspectClientMock.mockClear();
|
|
1920
|
+
createHostBridgeCdpClientMock.mockClear();
|
|
1600
1921
|
lastExtensionClient = undefined;
|
|
1601
1922
|
lastLocalClient = undefined;
|
|
1602
1923
|
lastCdpInspectClient = undefined;
|
|
1603
1924
|
cdpInspectEnabled = false;
|
|
1604
1925
|
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
1605
1926
|
_resetDesktopAutoCooldown();
|
|
1927
|
+
_resetHostBridgeCooldown();
|
|
1606
1928
|
logWarnCalls.length = 0;
|
|
1607
1929
|
logDebugCalls.length = 0;
|
|
1608
1930
|
mockSingletonProxy = null;
|
|
@@ -1811,12 +2133,14 @@ describe("buildPinnedCandidateList", () => {
|
|
|
1811
2133
|
createExtensionCdpClientMock.mockClear();
|
|
1812
2134
|
createLocalCdpClientMock.mockClear();
|
|
1813
2135
|
createCdpInspectClientMock.mockClear();
|
|
2136
|
+
createHostBridgeCdpClientMock.mockClear();
|
|
1814
2137
|
lastExtensionClient = undefined;
|
|
1815
2138
|
lastLocalClient = undefined;
|
|
1816
2139
|
lastCdpInspectClient = undefined;
|
|
1817
2140
|
cdpInspectEnabled = false;
|
|
1818
2141
|
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
1819
2142
|
_resetDesktopAutoCooldown();
|
|
2143
|
+
_resetHostBridgeCooldown();
|
|
1820
2144
|
mockSingletonProxy = null;
|
|
1821
2145
|
});
|
|
1822
2146
|
|
|
@@ -1892,6 +2216,54 @@ describe("buildPinnedCandidateList", () => {
|
|
|
1892
2216
|
expect(cdpErr.message).toContain("no Chrome Extension connected");
|
|
1893
2217
|
}
|
|
1894
2218
|
});
|
|
2219
|
+
|
|
2220
|
+
test("host-bridge mode produces single host-bridge candidate when bridge-only", () => {
|
|
2221
|
+
mockSingletonProxy = makeMacosBridgeOnlyProxy();
|
|
2222
|
+
const ctx = makeContext({ conversationId: "bpl-bridge" });
|
|
2223
|
+
|
|
2224
|
+
const candidates = buildPinnedCandidateList(ctx, "host-bridge");
|
|
2225
|
+
|
|
2226
|
+
expect(candidates).toHaveLength(1);
|
|
2227
|
+
expect(candidates[0].kind).toBe("host-bridge");
|
|
2228
|
+
expect(candidates[0].reason).toContain("pinned mode: host-bridge");
|
|
2229
|
+
});
|
|
2230
|
+
|
|
2231
|
+
test("host-bridge mode throws with diagnostics when proxy is unavailable", () => {
|
|
2232
|
+
mockSingletonProxy = makeUnavailableProxy();
|
|
2233
|
+
const ctx = makeContext({ conversationId: "bpl-bridge-absent" });
|
|
2234
|
+
|
|
2235
|
+
try {
|
|
2236
|
+
buildPinnedCandidateList(ctx, "host-bridge");
|
|
2237
|
+
expect(true).toBe(false); // should not reach
|
|
2238
|
+
} catch (err) {
|
|
2239
|
+
expect(err).toBeInstanceOf(CdpError);
|
|
2240
|
+
const cdpErr = err as CdpError;
|
|
2241
|
+
expect(cdpErr.code).toBe("transport_error");
|
|
2242
|
+
expect(cdpErr.attemptDiagnostics![0]).toMatchObject({
|
|
2243
|
+
candidateKind: "host-bridge",
|
|
2244
|
+
stage: "candidate_selection",
|
|
2245
|
+
});
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
|
|
2249
|
+
test("host-bridge mode throws when an extension has since connected", () => {
|
|
2250
|
+
// The sticky memo says host-bridge but a Chrome Extension is now
|
|
2251
|
+
// available. Throwing trips acquireCdpClientWithMode's sticky-fallback
|
|
2252
|
+
// retry, which clears the memo and re-runs auto so the conversation
|
|
2253
|
+
// upgrades to the extension candidate.
|
|
2254
|
+
mockSingletonProxy = makeAvailableProxy();
|
|
2255
|
+
const ctx = makeContext({ conversationId: "bpl-bridge-ext-upgrade" });
|
|
2256
|
+
|
|
2257
|
+
try {
|
|
2258
|
+
buildPinnedCandidateList(ctx, "host-bridge");
|
|
2259
|
+
expect(true).toBe(false); // should not reach
|
|
2260
|
+
} catch (err) {
|
|
2261
|
+
expect(err).toBeInstanceOf(CdpError);
|
|
2262
|
+
expect((err as CdpError).message).toContain(
|
|
2263
|
+
"a Chrome Extension is now connected",
|
|
2264
|
+
);
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
1895
2267
|
});
|
|
1896
2268
|
|
|
1897
2269
|
// ── Attempt diagnostics & fallback log tests ─────────────────────────────
|
|
@@ -1901,12 +2273,14 @@ describe("attempt diagnostics", () => {
|
|
|
1901
2273
|
createExtensionCdpClientMock.mockClear();
|
|
1902
2274
|
createLocalCdpClientMock.mockClear();
|
|
1903
2275
|
createCdpInspectClientMock.mockClear();
|
|
2276
|
+
createHostBridgeCdpClientMock.mockClear();
|
|
1904
2277
|
lastExtensionClient = undefined;
|
|
1905
2278
|
lastLocalClient = undefined;
|
|
1906
2279
|
lastCdpInspectClient = undefined;
|
|
1907
2280
|
cdpInspectEnabled = false;
|
|
1908
2281
|
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
1909
2282
|
_resetDesktopAutoCooldown();
|
|
2283
|
+
_resetHostBridgeCooldown();
|
|
1910
2284
|
logWarnCalls.length = 0;
|
|
1911
2285
|
logDebugCalls.length = 0;
|
|
1912
2286
|
mockSingletonProxy = null;
|
|
@@ -2300,12 +2674,14 @@ describe("macOS host-browser proxy without extension registry", () => {
|
|
|
2300
2674
|
createExtensionCdpClientMock.mockClear();
|
|
2301
2675
|
createLocalCdpClientMock.mockClear();
|
|
2302
2676
|
createCdpInspectClientMock.mockClear();
|
|
2677
|
+
createHostBridgeCdpClientMock.mockClear();
|
|
2303
2678
|
lastExtensionClient = undefined;
|
|
2304
2679
|
lastLocalClient = undefined;
|
|
2305
2680
|
lastCdpInspectClient = undefined;
|
|
2306
2681
|
cdpInspectEnabled = false;
|
|
2307
2682
|
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
2308
2683
|
_resetDesktopAutoCooldown();
|
|
2684
|
+
_resetHostBridgeCooldown();
|
|
2309
2685
|
logWarnCalls.length = 0;
|
|
2310
2686
|
logDebugCalls.length = 0;
|
|
2311
2687
|
mockSingletonProxy = null;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { HostBrowserProxy } from "../../../../daemon/host-browser-proxy.js";
|
|
4
|
+
import { CdpError } from "../errors.js";
|
|
5
|
+
import {
|
|
6
|
+
createHostBridgeCdpClient,
|
|
7
|
+
HostBridgeCdpClient,
|
|
8
|
+
} from "../host-bridge-cdp-client.js";
|
|
9
|
+
|
|
10
|
+
type ProxyResult = { content: string; isError: boolean };
|
|
11
|
+
|
|
12
|
+
function fakeProxy(
|
|
13
|
+
handler: (
|
|
14
|
+
input: unknown,
|
|
15
|
+
conversationId: string,
|
|
16
|
+
signal?: AbortSignal,
|
|
17
|
+
) => Promise<ProxyResult> | ProxyResult,
|
|
18
|
+
): {
|
|
19
|
+
proxy: HostBrowserProxy;
|
|
20
|
+
request: ReturnType<typeof mock>;
|
|
21
|
+
} {
|
|
22
|
+
const request = mock(
|
|
23
|
+
async (
|
|
24
|
+
input: unknown,
|
|
25
|
+
conversationId: string,
|
|
26
|
+
signal?: AbortSignal,
|
|
27
|
+
): Promise<ProxyResult> => handler(input, conversationId, signal),
|
|
28
|
+
);
|
|
29
|
+
const proxy = { request } as unknown as HostBrowserProxy;
|
|
30
|
+
return { proxy, request };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("HostBridgeCdpClient", () => {
|
|
34
|
+
test("kind is 'host-bridge' and exposes conversationId", () => {
|
|
35
|
+
const { proxy } = fakeProxy(async () => ({
|
|
36
|
+
content: "{}",
|
|
37
|
+
isError: false,
|
|
38
|
+
}));
|
|
39
|
+
const client = createHostBridgeCdpClient(proxy, "conv-bridge");
|
|
40
|
+
expect(client).toBeInstanceOf(HostBridgeCdpClient);
|
|
41
|
+
expect(client.kind).toBe("host-bridge");
|
|
42
|
+
expect(client.conversationId).toBe("conv-bridge");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("send forwards raw CDP with no cdpSessionId or targetClientId", async () => {
|
|
46
|
+
const { proxy, request } = fakeProxy(async () => ({
|
|
47
|
+
content: JSON.stringify({ frameId: "frame-1" }),
|
|
48
|
+
isError: false,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const client = createHostBridgeCdpClient(proxy, "conv-1", "actor-1");
|
|
52
|
+
const result = await client.send<{ frameId: string }>("Page.navigate", {
|
|
53
|
+
url: "https://example.com/",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result).toEqual({ frameId: "frame-1" });
|
|
57
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
58
|
+
const call = request.mock.calls[0];
|
|
59
|
+
expect(call?.[0]).toEqual({
|
|
60
|
+
cdpMethod: "Page.navigate",
|
|
61
|
+
cdpParams: { url: "https://example.com/" },
|
|
62
|
+
cdpSessionId: undefined,
|
|
63
|
+
});
|
|
64
|
+
expect(call?.[1]).toBe("conv-1");
|
|
65
|
+
// sourceActorPrincipalId threads through; targetClientId is never set.
|
|
66
|
+
expect(call?.[3]).toBe("actor-1");
|
|
67
|
+
expect(call?.[4]).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("bridge 'unreachable' envelope classifies as transport_error (failover-eligible)", async () => {
|
|
71
|
+
const { proxy } = fakeProxy(async () => ({
|
|
72
|
+
content: JSON.stringify({
|
|
73
|
+
code: "unreachable",
|
|
74
|
+
message: "HTTP 404 from http://localhost:9222/json/list",
|
|
75
|
+
}),
|
|
76
|
+
isError: true,
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
const client = createHostBridgeCdpClient(proxy, "conv-err");
|
|
80
|
+
try {
|
|
81
|
+
await client.send("Page.captureScreenshot", {});
|
|
82
|
+
expect(true).toBe(false); // should not reach
|
|
83
|
+
} catch (err) {
|
|
84
|
+
expect(err).toBeInstanceOf(CdpError);
|
|
85
|
+
expect((err as CdpError).code).toBe("transport_error");
|
|
86
|
+
expect((err as CdpError).message).toContain("HTTP 404");
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test.each(["listTabs", "selectTab", "closeTab"] as const)(
|
|
91
|
+
"%s throws transport_error locally without touching the proxy",
|
|
92
|
+
async (method) => {
|
|
93
|
+
const { proxy, request } = fakeProxy(async () => ({
|
|
94
|
+
content: "{}",
|
|
95
|
+
isError: false,
|
|
96
|
+
}));
|
|
97
|
+
const client = createHostBridgeCdpClient(proxy, "conv-tabs");
|
|
98
|
+
|
|
99
|
+
const call =
|
|
100
|
+
method === "listTabs" ? client.listTabs() : client[method](1);
|
|
101
|
+
await expect(call).rejects.toThrow(
|
|
102
|
+
`${method} is not supported by the host-bridge backend (extension backend required)`,
|
|
103
|
+
);
|
|
104
|
+
expect(request).not.toHaveBeenCalled();
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
});
|
|
@@ -79,7 +79,12 @@ describe("cdp-client re-exports", () => {
|
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
test("ScopedCdpClient exposes kind and conversationId", () => {
|
|
82
|
-
const kinds: CdpClientKind[] = [
|
|
82
|
+
const kinds: CdpClientKind[] = [
|
|
83
|
+
"local",
|
|
84
|
+
"extension",
|
|
85
|
+
"cdp-inspect",
|
|
86
|
+
"host-bridge",
|
|
87
|
+
];
|
|
83
88
|
for (const kind of kinds) {
|
|
84
89
|
const scoped: ScopedCdpClient = {
|
|
85
90
|
kind,
|