@vellumai/assistant 0.6.5 → 0.6.6
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/AGENTS.md +9 -1
- package/ARCHITECTURE.md +15 -17
- package/Dockerfile +6 -4
- package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
- package/docs/architecture/integrations.md +32 -39
- package/docs/architecture/memory.md +25 -30
- package/docs/architecture/security.md +7 -6
- package/docs/browser-use-architecture-phase2.md +63 -20
- package/docs/plugins.md +761 -0
- package/examples/plugins/echo/README.md +132 -0
- package/examples/plugins/echo/package.json +17 -0
- package/examples/plugins/echo/register.ts +187 -0
- package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
- package/openapi.yaml +212 -68
- package/package.json +1 -1
- package/src/__tests__/app-compiler.test.ts +57 -0
- package/src/__tests__/approval-cascade.test.ts +7 -2
- package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
- package/src/__tests__/avatar-generator.test.ts +4 -2
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/catalog-cache.test.ts +69 -0
- package/src/__tests__/checker.test.ts +459 -171
- package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
- package/src/__tests__/compaction-events.test.ts +501 -0
- package/src/__tests__/compaction-pipeline.test.ts +210 -0
- package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
- package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
- package/src/__tests__/config-model-image-provider.test.ts +110 -0
- package/src/__tests__/config-schema.test.ts +22 -9
- package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
- package/src/__tests__/contacts-tools.test.ts +26 -0
- package/src/__tests__/context-overflow-policy.test.ts +7 -7
- package/src/__tests__/context-window-manager.test.ts +355 -4
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
- package/src/__tests__/conversation-agent-loop.test.ts +30 -141
- package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
- package/src/__tests__/conversation-pairing.test.ts +174 -10
- package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
- package/src/__tests__/conversation-process-callsite.test.ts +3 -0
- package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
- package/src/__tests__/conversation-queue.test.ts +29 -14
- package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
- package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
- package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
- package/src/__tests__/conversation-seed-composer.test.ts +2 -2
- package/src/__tests__/conversation-slash-queue.test.ts +7 -2
- package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
- package/src/__tests__/conversation-speed-override.test.ts +6 -1
- package/src/__tests__/conversation-title-service.test.ts +116 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
- package/src/__tests__/conversation-usage.test.ts +1 -1
- package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
- package/src/__tests__/credential-health-service.test.ts +78 -9
- package/src/__tests__/credential-security-invariants.test.ts +2 -2
- package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
- package/src/__tests__/empty-response-pipeline.test.ts +305 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
- package/src/__tests__/first-greeting.test.ts +247 -5
- package/src/__tests__/headless-browser-mode.test.ts +57 -0
- package/src/__tests__/history-repair-pipeline.test.ts +399 -0
- package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
- package/src/__tests__/host-proxy-interface.test.ts +36 -2
- package/src/__tests__/image-credentials.test.ts +137 -0
- package/src/__tests__/image-service-dispatcher.test.ts +186 -0
- package/src/__tests__/injector-chain.test.ts +526 -0
- package/src/__tests__/intent-routing.test.ts +0 -26
- package/src/__tests__/llm-call-pipeline.test.ts +285 -0
- package/src/__tests__/llm-schema.test.ts +1 -1
- package/src/__tests__/media-generate-image.test.ts +119 -13
- package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
- package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
- package/src/__tests__/migration-import-from-url.test.ts +5 -68
- package/src/__tests__/model-intents.test.ts +4 -2
- package/src/__tests__/notification-broadcaster.test.ts +3 -3
- package/src/__tests__/notification-decision-strategy.test.ts +0 -11
- package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
- package/src/__tests__/oauth-apps-routes.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +14 -12
- package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
- package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
- package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
- package/src/__tests__/oauth-providers-routes.test.ts +3 -2
- package/src/__tests__/oauth-store.test.ts +41 -76
- package/src/__tests__/onboarding-template-contract.test.ts +16 -64
- package/src/__tests__/openai-image-service.test.ts +368 -0
- package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
- package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
- package/src/__tests__/persistence-pipeline.test.ts +377 -0
- package/src/__tests__/pipeline-runner.test.ts +565 -0
- package/src/__tests__/platform.test.ts +5 -2
- package/src/__tests__/plugin-bootstrap.test.ts +483 -0
- package/src/__tests__/plugin-registry.test.ts +273 -0
- package/src/__tests__/plugin-route-contribution.test.ts +288 -0
- package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
- package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
- package/src/__tests__/plugin-types.test.ts +320 -0
- package/src/__tests__/pricing.test.ts +44 -12
- package/src/__tests__/proxy-approval-callback.test.ts +69 -8
- package/src/__tests__/reaction-persistence.test.ts +1 -0
- package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
- package/src/__tests__/registry.test.ts +0 -2
- package/src/__tests__/schedule-routes.test.ts +131 -1
- package/src/__tests__/scheduler-recurrence.test.ts +14 -70
- package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
- package/src/__tests__/secret-detection-handler.test.ts +0 -10
- package/src/__tests__/shell-identity.test.ts +0 -134
- package/src/__tests__/suggestion-routes.test.ts +103 -4
- package/src/__tests__/task-memory-cleanup.test.ts +1 -0
- package/src/__tests__/task-scheduler.test.ts +3 -15
- package/src/__tests__/test-preload.ts +11 -0
- package/src/__tests__/title-generate-pipeline.test.ts +224 -0
- package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
- package/src/__tests__/tool-error-pipeline.test.ts +244 -0
- package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
- package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
- package/src/__tests__/tool-executor.test.ts +141 -0
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
- package/src/__tests__/tool-result-truncation.test.ts +0 -110
- package/src/__tests__/user-plugin-loader.test.ts +191 -0
- package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
- package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
- package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
- package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
- package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
- package/src/__tests__/workspace-policy.test.ts +21 -3
- package/src/agent/loop.ts +340 -102
- package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
- package/src/approvals/guardian-request-resolvers.ts +80 -0
- package/src/backup/__tests__/backup-worker.test.ts +2 -13
- package/src/backup/backup-worker.ts +3 -15
- package/src/bundler/app-compiler.ts +84 -1
- package/src/calls/call-state.ts +2 -2
- package/src/channels/__tests__/types.test.ts +3 -3
- package/src/channels/types.ts +6 -4
- package/src/cli/__tests__/notifications.test.ts +87 -211
- package/src/cli/commands/__tests__/backup.test.ts +1 -1
- package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
- package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
- package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
- package/src/cli/commands/backup.ts +2 -2
- package/src/cli/commands/clients.ts +138 -0
- package/src/cli/commands/completions.ts +2 -9
- package/src/cli/commands/conversations.ts +55 -7
- package/src/cli/commands/image-generation.ts +33 -34
- package/src/cli/commands/notifications.ts +68 -103
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +2 -2
- package/src/cli/commands/oauth/providers.ts +176 -8
- package/src/cli/commands/oauth/status.ts +46 -36
- package/src/cli/commands/skills.ts +3 -4
- package/src/cli/program.ts +25 -29
- package/src/config/__tests__/backup-schema.test.ts +7 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
- package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
- package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
- package/src/config/bundled-skills/messaging/SKILL.md +3 -3
- package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
- package/src/config/bundled-skills/schedule/SKILL.md +8 -3
- package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
- package/src/config/bundled-tool-registry.ts +0 -15
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +19 -0
- package/src/config/schemas/backup.ts +1 -1
- package/src/config/schemas/conversations.ts +16 -0
- package/src/config/schemas/llm.ts +2 -3
- package/src/config/schemas/security.ts +6 -6
- package/src/config/schemas/tts.ts +11 -0
- package/src/config/skill-state.ts +6 -2
- package/src/config/skills.ts +94 -5
- package/src/context/__tests__/compact-prompt.test.ts +27 -9
- package/src/context/prompts/compact.md +26 -12
- package/src/context/tool-result-truncation.ts +3 -63
- package/src/context/window-manager.ts +190 -16
- package/src/credential-health/credential-health-service.ts +19 -6
- package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
- package/src/daemon/config-watcher.ts +0 -2
- package/src/daemon/context-overflow-policy.ts +4 -13
- package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
- package/src/daemon/conversation-agent-loop.ts +984 -683
- package/src/daemon/conversation-history.ts +10 -19
- package/src/daemon/conversation-lifecycle.ts +37 -19
- package/src/daemon/conversation-notifiers.ts +2 -110
- package/src/daemon/conversation-process.ts +14 -7
- package/src/daemon/conversation-runtime-assembly.ts +532 -411
- package/src/daemon/conversation-tool-setup.ts +41 -4
- package/src/daemon/conversation.ts +80 -35
- package/src/daemon/external-plugins-bootstrap.ts +478 -0
- package/src/daemon/first-greeting.ts +191 -14
- package/src/daemon/handlers/config-model.ts +11 -0
- package/src/daemon/handlers/skills.ts +5 -1
- package/src/daemon/lifecycle.ts +33 -68
- package/src/daemon/message-types/computer-use.ts +2 -34
- package/src/daemon/message-types/conversations.ts +49 -0
- package/src/daemon/message-types/messages.ts +12 -0
- package/src/daemon/server.ts +5 -3
- package/src/daemon/shutdown-handlers.ts +2 -12
- package/src/daemon/tool-side-effects.ts +14 -56
- package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
- package/src/heartbeat/heartbeat-service.ts +24 -1
- package/src/home/__tests__/feed-population-integration.test.ts +312 -0
- package/src/home/emit-feed-event.ts +7 -0
- package/src/home/feed-types.ts +41 -2
- package/src/home/rewrite-command-preview.ts +66 -0
- package/src/ipc/__tests__/socket-path.test.ts +11 -50
- package/src/ipc/cli-client.ts +1 -1
- package/src/ipc/cli-server.ts +3 -3
- package/src/ipc/gateway-client.ts +4 -1
- package/src/ipc/routes/browser-context.ts +2 -0
- package/src/ipc/routes/browser.ts +1 -0
- package/src/ipc/routes/get-contact.ts +16 -0
- package/src/ipc/routes/index.ts +14 -0
- package/src/ipc/routes/list-clients.ts +31 -0
- package/src/ipc/routes/merge-contacts.ts +17 -0
- package/src/ipc/routes/notification.ts +133 -0
- package/src/ipc/routes/rename-conversation.ts +59 -0
- package/src/ipc/routes/search-contacts.ts +19 -0
- package/src/ipc/routes/upsert-contact.ts +25 -0
- package/src/ipc/socket-path.ts +14 -38
- package/src/media/app-icon-generator.ts +23 -46
- package/src/media/avatar-router.ts +26 -41
- package/src/media/gemini-image-service.ts +8 -41
- package/src/media/image-credentials.ts +73 -0
- package/src/media/image-service.ts +85 -0
- package/src/media/openai-image-service.ts +131 -0
- package/src/media/types.ts +46 -0
- package/src/memory/conversation-crud.ts +48 -18
- package/src/memory/conversation-queries.ts +57 -4
- package/src/memory/conversation-title-service.ts +25 -0
- package/src/memory/db-init.ts +8 -0
- package/src/memory/embedding-gemini.test.ts +41 -2
- package/src/memory/embedding-gemini.ts +6 -1
- package/src/memory/graph/bootstrap.test.ts +282 -0
- package/src/memory/graph/bootstrap.ts +8 -5
- package/src/memory/graph/extraction.ts +10 -2
- package/src/memory/graph/graph-search.test.ts +1 -0
- package/src/memory/graph/inspect.ts +2 -2
- package/src/memory/graph/retriever.ts +10 -3
- package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
- package/src/memory/migrations/149-oauth-tables.ts +1 -0
- package/src/memory/migrations/223-schedule-script-column.ts +11 -0
- package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
- package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
- package/src/memory/migrations/index.ts +4 -0
- package/src/memory/pkb/pkb-index.test.ts +1 -0
- package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
- package/src/memory/pkb/pkb-search.test.ts +65 -4
- package/src/memory/pkb/pkb-search.ts +40 -18
- package/src/memory/qdrant-client.test.ts +60 -0
- package/src/memory/qdrant-client.ts +25 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/schema/oauth.ts +4 -1
- package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
- package/src/messaging/providers/slack/render-transcript.ts +58 -0
- package/src/notifications/conversation-pairing.ts +78 -19
- package/src/notifications/copy-composer.ts +0 -5
- package/src/notifications/emit-signal.ts +1 -1
- package/src/notifications/signal.ts +1 -2
- package/src/oauth/AGENTS.md +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
- package/src/oauth/connect-orchestrator.ts +8 -34
- package/src/oauth/connect-types.ts +6 -10
- package/src/oauth/manual-token-connection.ts +23 -0
- package/src/oauth/oauth-store.ts +30 -14
- package/src/oauth/provider-serializer.ts +6 -1
- package/src/oauth/seed-providers.ts +56 -108
- package/src/outbound-proxy/http-forwarder.ts +9 -0
- package/src/permissions/approval-policy.test.ts +293 -18
- package/src/permissions/approval-policy.ts +110 -58
- package/src/permissions/arg-parser.test.ts +161 -0
- package/src/permissions/arg-parser.ts +141 -0
- package/src/permissions/bash-risk-classifier.test.ts +414 -2
- package/src/permissions/bash-risk-classifier.ts +303 -60
- package/src/permissions/checker.ts +157 -29
- package/src/permissions/command-registry.test.ts +239 -0
- package/src/permissions/command-registry.ts +234 -54
- package/src/permissions/defaults.ts +5 -4
- package/src/permissions/gateway-threshold-reader.ts +196 -0
- package/src/permissions/prompter.ts +4 -0
- package/src/permissions/risk-types.ts +61 -4
- package/src/permissions/schedule-risk-classifier.test.ts +129 -0
- package/src/permissions/schedule-risk-classifier.ts +85 -0
- package/src/permissions/shell-identity.ts +2 -42
- package/src/permissions/types.ts +2 -0
- package/src/permissions/workspace-policy.ts +8 -3
- package/src/plugins/defaults/circuit-breaker.ts +146 -0
- package/src/plugins/defaults/compaction.ts +145 -0
- package/src/plugins/defaults/empty-response.ts +126 -0
- package/src/plugins/defaults/history-repair.ts +85 -0
- package/src/plugins/defaults/index.ts +116 -0
- package/src/plugins/defaults/injectors.ts +491 -0
- package/src/plugins/defaults/llm-call.ts +82 -0
- package/src/plugins/defaults/memory-retrieval.ts +226 -0
- package/src/plugins/defaults/overflow-reduce.ts +181 -0
- package/src/plugins/defaults/persistence.ts +129 -0
- package/src/plugins/defaults/title-generate.ts +95 -0
- package/src/plugins/defaults/token-estimate.ts +104 -0
- package/src/plugins/defaults/tool-error.ts +126 -0
- package/src/plugins/defaults/tool-execute.ts +89 -0
- package/src/plugins/defaults/tool-result-truncate.ts +88 -0
- package/src/plugins/pipeline.ts +316 -0
- package/src/plugins/plugin-skill-contributions.ts +292 -0
- package/src/plugins/registry.ts +241 -0
- package/src/plugins/types.ts +1134 -0
- package/src/plugins/user-loader.ts +177 -0
- package/src/prompts/templates/BOOTSTRAP.md +27 -77
- package/src/providers/model-catalog.ts +52 -29
- package/src/providers/model-intents.ts +1 -1
- package/src/providers/openrouter/client.ts +5 -1
- package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
- package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
- package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
- package/src/providers/speech-to-text/xai-realtime.ts +39 -14
- package/src/runtime/AGENTS.md +25 -16
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
- package/src/runtime/__tests__/client-registry.test.ts +293 -0
- package/src/runtime/client-registry.ts +261 -0
- package/src/runtime/http-server.ts +77 -8
- package/src/runtime/http-types.ts +0 -2
- package/src/runtime/migrations/vbundle-builder.ts +1 -22
- package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
- package/src/runtime/routes/approval-routes.ts +17 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
- package/src/runtime/routes/conversation-routes.ts +223 -116
- package/src/runtime/routes/inbound-message-handler.ts +88 -13
- package/src/runtime/routes/memory-item-routes.test.ts +1 -0
- package/src/runtime/routes/migration-routes.ts +0 -3
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
- package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
- package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
- package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
- package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
- package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
- package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
- package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
- package/src/runtime/routes/playground/deps.ts +56 -0
- package/src/runtime/routes/playground/force-compact.ts +73 -0
- package/src/runtime/routes/playground/guard.ts +37 -0
- package/src/runtime/routes/playground/index.ts +28 -0
- package/src/runtime/routes/playground/inject-failures.ts +159 -0
- package/src/runtime/routes/playground/reset-circuit.ts +115 -0
- package/src/runtime/routes/playground/seed-conversation.ts +139 -0
- package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
- package/src/runtime/routes/playground/state.ts +78 -0
- package/src/runtime/routes/schedule-routes.ts +89 -8
- package/src/runtime/skill-route-registry.ts +75 -15
- package/src/schedule/run-script.ts +68 -0
- package/src/schedule/schedule-store.ts +7 -1
- package/src/schedule/scheduler.ts +48 -8
- package/src/skills/catalog-cache.ts +12 -5
- package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
- package/src/tools/browser/browser-execution.ts +88 -19
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
- package/src/tools/browser/cdp-client/factory.ts +15 -4
- package/src/tools/executor.ts +126 -74
- package/src/tools/network/script-proxy/session-manager.ts +37 -1
- package/src/tools/permission-checker.ts +98 -49
- package/src/tools/policy-context.ts +4 -0
- package/src/tools/registry.ts +140 -3
- package/src/tools/schedule/create.ts +23 -8
- package/src/tools/schedule/update.ts +3 -1
- package/src/tools/secret-detection-handler.ts +0 -51
- package/src/tools/system/avatar-generator.ts +6 -2
- package/src/tools/types.ts +28 -2
- package/src/util/platform.ts +7 -2
- package/src/util/pricing.ts +26 -3
- package/src/workspace/migrations/006-services-config.ts +2 -4
- package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
- package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
- package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
- package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
- package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
- package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
- package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
- package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
- package/src/workspace/migrations/registry.ts +12 -0
- package/tsconfig.json +1 -1
- package/hook-templates/debug-prompt-logger/hook.json +0 -7
- package/hook-templates/debug-prompt-logger/run.sh +0 -66
- package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
- package/src/__tests__/context-overflow-approval.test.ts +0 -156
- package/src/__tests__/hooks-blocking.test.ts +0 -178
- package/src/__tests__/hooks-cli.test.ts +0 -182
- package/src/__tests__/hooks-config.test.ts +0 -108
- package/src/__tests__/hooks-discovery.test.ts +0 -211
- package/src/__tests__/hooks-integration.test.ts +0 -196
- package/src/__tests__/hooks-manager.test.ts +0 -226
- package/src/__tests__/hooks-runner.test.ts +0 -175
- package/src/__tests__/hooks-settings.test.ts +0 -160
- package/src/__tests__/hooks-templates.test.ts +0 -169
- package/src/__tests__/hooks-ts-runner.test.ts +0 -170
- package/src/__tests__/hooks-watch.test.ts +0 -112
- package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
- package/src/__tests__/oauth-scope-policy.test.ts +0 -180
- package/src/__tests__/send-notification-tool.test.ts +0 -83
- package/src/cli/commands/shotgun.ts +0 -266
- package/src/config/bundled-skills/conversations/SKILL.md +0 -20
- package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
- package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
- package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
- package/src/config/bundled-skills/notifications/SKILL.md +0 -40
- package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
- package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
- package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
- package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
- package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
- package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
- package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
- package/src/daemon/context-overflow-approval.ts +0 -52
- package/src/daemon/watch-handler.ts +0 -399
- package/src/hooks/cli.ts +0 -253
- package/src/hooks/config.ts +0 -100
- package/src/hooks/discovery.ts +0 -135
- package/src/hooks/manager.ts +0 -179
- package/src/hooks/runner.ts +0 -117
- package/src/hooks/templates.ts +0 -77
- package/src/hooks/types.ts +0 -75
- package/src/oauth/scope-policy.ts +0 -89
- package/src/runtime/gateway-internal-client.ts +0 -94
- package/src/runtime/routes/watch-routes.ts +0 -156
- package/src/signals/shotgun.ts +0 -203
- package/src/tools/watch/screen-watch.ts +0 -144
- package/src/tools/watch/watch-state.ts +0 -142
|
@@ -1058,22 +1058,43 @@ describe("desktop-auto cdp-inspect (macOS)", () => {
|
|
|
1058
1058
|
expect(candidates[2].kind).toBe("local");
|
|
1059
1059
|
});
|
|
1060
1060
|
|
|
1061
|
-
test("macOS turn with proxy unavailable skips desktop-auto cdp-inspect (extension intent)", () => {
|
|
1061
|
+
test("macOS turn with registry-routed proxy unavailable skips desktop-auto cdp-inspect (extension intent)", () => {
|
|
1062
1062
|
const fakeProxy = makeUnavailableProxy();
|
|
1063
1063
|
const ctx = makeContext({
|
|
1064
1064
|
conversationId: "macos-proxy-unavailable-no-inspect",
|
|
1065
1065
|
hostBrowserProxy: fakeProxy,
|
|
1066
1066
|
transportInterface: "macos",
|
|
1067
|
+
hostBrowserRegistryRouted: true,
|
|
1067
1068
|
});
|
|
1068
1069
|
|
|
1069
1070
|
const candidates = buildCandidateList(ctx);
|
|
1070
1071
|
|
|
1071
1072
|
// Should only include local -- cdp-inspect is suppressed because extension
|
|
1072
|
-
// transport is expected (proxy
|
|
1073
|
+
// transport is expected (registry-routed proxy) but temporarily unavailable.
|
|
1073
1074
|
expect(candidates.length).toBe(1);
|
|
1074
1075
|
expect(candidates[0].kind).toBe("local");
|
|
1075
1076
|
});
|
|
1076
1077
|
|
|
1078
|
+
test("macOS turn with SSE-backed proxy unavailable still includes desktop-auto cdp-inspect", () => {
|
|
1079
|
+
const fakeProxy = makeUnavailableProxy();
|
|
1080
|
+
const ctx = makeContext({
|
|
1081
|
+
conversationId: "macos-sse-proxy-unavailable-inspect-allowed",
|
|
1082
|
+
hostBrowserProxy: fakeProxy,
|
|
1083
|
+
transportInterface: "macos",
|
|
1084
|
+
// hostBrowserRegistryRouted is NOT set -- SSE-backed proxy
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
const candidates = buildCandidateList(ctx);
|
|
1088
|
+
|
|
1089
|
+
// SSE-backed proxy that is unavailable (non-interactive turn) should NOT
|
|
1090
|
+
// suppress cdp-inspect -- the SSE proxy was never expected to service
|
|
1091
|
+
// browser requests, so cdp-inspect remains available as a fallback.
|
|
1092
|
+
expect(candidates.length).toBe(2);
|
|
1093
|
+
expect(candidates[0].kind).toBe("cdp-inspect");
|
|
1094
|
+
expect(candidates[0].reason).toContain("desktopAuto");
|
|
1095
|
+
expect(candidates[1].kind).toBe("local");
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1077
1098
|
test("macOS turn with no proxy still includes desktop-auto cdp-inspect", () => {
|
|
1078
1099
|
const ctx = makeContext({
|
|
1079
1100
|
conversationId: "macos-no-proxy-inspect-allowed",
|
|
@@ -1242,17 +1263,19 @@ describe("desktop-auto cdp-inspect (macOS)", () => {
|
|
|
1242
1263
|
expect(candidates[0].kind).toBe("local");
|
|
1243
1264
|
});
|
|
1244
1265
|
|
|
1245
|
-
test("macOS turn with proxy unavailable routes to local without trying cdp-inspect", async () => {
|
|
1266
|
+
test("macOS turn with registry-routed proxy unavailable routes to local without trying cdp-inspect", async () => {
|
|
1246
1267
|
const fakeProxy = makeUnavailableProxy();
|
|
1247
1268
|
const ctx = makeContext({
|
|
1248
1269
|
conversationId: "macos-proxy-unavail-route",
|
|
1249
1270
|
hostBrowserProxy: fakeProxy,
|
|
1250
1271
|
transportInterface: "macos",
|
|
1272
|
+
hostBrowserRegistryRouted: true,
|
|
1251
1273
|
});
|
|
1252
1274
|
|
|
1253
1275
|
const client = getCdpClient(ctx);
|
|
1254
1276
|
|
|
1255
1277
|
// Should go straight to local -- no cdp-inspect candidate inserted
|
|
1278
|
+
// because the registry-routed extension was expected but is unavailable.
|
|
1256
1279
|
expect(client.kind).toBe("local");
|
|
1257
1280
|
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1258
1281
|
"Page.navigate",
|
|
@@ -1264,6 +1287,29 @@ describe("desktop-auto cdp-inspect (macOS)", () => {
|
|
|
1264
1287
|
client.dispose();
|
|
1265
1288
|
});
|
|
1266
1289
|
|
|
1290
|
+
test("macOS turn with SSE-backed proxy unavailable falls through to cdp-inspect", async () => {
|
|
1291
|
+
const fakeProxy = makeUnavailableProxy();
|
|
1292
|
+
const ctx = makeContext({
|
|
1293
|
+
conversationId: "macos-sse-proxy-unavail-inspect",
|
|
1294
|
+
hostBrowserProxy: fakeProxy,
|
|
1295
|
+
transportInterface: "macos",
|
|
1296
|
+
// hostBrowserRegistryRouted is NOT set -- SSE-backed proxy
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
const client = getCdpClient(ctx);
|
|
1300
|
+
|
|
1301
|
+
// SSE-backed proxy unavailable (non-interactive turn) should NOT
|
|
1302
|
+
// suppress cdp-inspect -- it falls through to desktop-auto cdp-inspect.
|
|
1303
|
+
expect(client.kind).toBe("cdp-inspect");
|
|
1304
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
1305
|
+
"Page.navigate",
|
|
1306
|
+
);
|
|
1307
|
+
expect(result).toEqual({ ok: true, via: "cdp-inspect" });
|
|
1308
|
+
expect(createExtensionCdpClientMock).not.toHaveBeenCalled();
|
|
1309
|
+
expect(createCdpInspectClientMock).toHaveBeenCalledTimes(1);
|
|
1310
|
+
client.dispose();
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1267
1313
|
test("explicit config cdp-inspect failure does NOT record desktop-auto cooldown", async () => {
|
|
1268
1314
|
cdpInspectEnabled = true;
|
|
1269
1315
|
|
|
@@ -1991,3 +2037,100 @@ describe("no-fallback guarantees", () => {
|
|
|
1991
2037
|
expect(fallbackLogs.length).toBe(0);
|
|
1992
2038
|
});
|
|
1993
2039
|
});
|
|
2040
|
+
|
|
2041
|
+
// ── macOS host-browser proxy backend selection ─────────────────────────
|
|
2042
|
+
//
|
|
2043
|
+
// Verify that macOS turns can use the host browser proxy without requiring
|
|
2044
|
+
// extension registry connectivity. When a HostBrowserProxy is provisioned
|
|
2045
|
+
// via the SSE sender path (no extension), the factory should select
|
|
2046
|
+
// extension as the top candidate (because hostBrowserProxy is available).
|
|
2047
|
+
// When both proxy and fallback backends exist, selection is deterministic:
|
|
2048
|
+
// extension > cdp-inspect > local.
|
|
2049
|
+
|
|
2050
|
+
describe("macOS host-browser proxy without extension registry", () => {
|
|
2051
|
+
beforeEach(() => {
|
|
2052
|
+
createExtensionCdpClientMock.mockClear();
|
|
2053
|
+
createLocalCdpClientMock.mockClear();
|
|
2054
|
+
createCdpInspectClientMock.mockClear();
|
|
2055
|
+
lastExtensionClient = undefined;
|
|
2056
|
+
lastLocalClient = undefined;
|
|
2057
|
+
lastCdpInspectClient = undefined;
|
|
2058
|
+
cdpInspectEnabled = false;
|
|
2059
|
+
desktopAutoConfig = { enabled: true, cooldownMs: 30_000 };
|
|
2060
|
+
_resetDesktopAutoCooldown();
|
|
2061
|
+
logWarnCalls.length = 0;
|
|
2062
|
+
logDebugCalls.length = 0;
|
|
2063
|
+
});
|
|
2064
|
+
|
|
2065
|
+
test("macOS turn with SSE-provisioned hostBrowserProxy selects extension backend", async () => {
|
|
2066
|
+
// Simulates macOS provisioning a HostBrowserProxy via SSE (no extension
|
|
2067
|
+
// registry connection). The proxy is available so extension is selected.
|
|
2068
|
+
const fakeProxy = makeAvailableProxy();
|
|
2069
|
+
const ctx = makeContext({
|
|
2070
|
+
conversationId: "macos-sse-proxy",
|
|
2071
|
+
hostBrowserProxy: fakeProxy,
|
|
2072
|
+
transportInterface: "macos",
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
const client = getCdpClient(ctx);
|
|
2076
|
+
|
|
2077
|
+
expect(client.kind).toBe("extension");
|
|
2078
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
2079
|
+
"Page.navigate",
|
|
2080
|
+
{ url: "https://example.com" },
|
|
2081
|
+
);
|
|
2082
|
+
expect(result).toEqual({ ok: true, via: "extension" });
|
|
2083
|
+
expect(createExtensionCdpClientMock).toHaveBeenCalledTimes(1);
|
|
2084
|
+
});
|
|
2085
|
+
|
|
2086
|
+
test("macOS turn with both proxy and cdp-inspect produces deterministic 3-candidate chain", () => {
|
|
2087
|
+
const fakeProxy = makeAvailableProxy();
|
|
2088
|
+
const ctx = makeContext({
|
|
2089
|
+
conversationId: "macos-deterministic",
|
|
2090
|
+
hostBrowserProxy: fakeProxy,
|
|
2091
|
+
transportInterface: "macos",
|
|
2092
|
+
});
|
|
2093
|
+
|
|
2094
|
+
const candidates = buildCandidateList(ctx);
|
|
2095
|
+
|
|
2096
|
+
// Deterministic order: extension > cdp-inspect (desktop-auto) > local
|
|
2097
|
+
expect(candidates.length).toBe(3);
|
|
2098
|
+
expect(candidates[0].kind).toBe("extension");
|
|
2099
|
+
expect(candidates[1].kind).toBe("cdp-inspect");
|
|
2100
|
+
expect(candidates[2].kind).toBe("local");
|
|
2101
|
+
});
|
|
2102
|
+
|
|
2103
|
+
test("macOS turn without proxy falls through to cdp-inspect then local", () => {
|
|
2104
|
+
const ctx = makeContext({
|
|
2105
|
+
conversationId: "macos-no-proxy-fallback",
|
|
2106
|
+
transportInterface: "macos",
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
const candidates = buildCandidateList(ctx);
|
|
2110
|
+
|
|
2111
|
+
// No proxy => skip extension, desktop-auto cdp-inspect + local
|
|
2112
|
+
expect(candidates.length).toBe(2);
|
|
2113
|
+
expect(candidates[0].kind).toBe("cdp-inspect");
|
|
2114
|
+
expect(candidates[1].kind).toBe("local");
|
|
2115
|
+
});
|
|
2116
|
+
|
|
2117
|
+
test("non-macOS interface with proxy still selects extension (unchanged behavior)", async () => {
|
|
2118
|
+
// Verify non-macOS interfaces are unaffected by the macOS host-browser
|
|
2119
|
+
// enablement — proxy presence drives extension selection regardless of
|
|
2120
|
+
// interface.
|
|
2121
|
+
const fakeProxy = makeAvailableProxy();
|
|
2122
|
+
const ctx = makeContext({
|
|
2123
|
+
conversationId: "non-macos-proxy",
|
|
2124
|
+
hostBrowserProxy: fakeProxy,
|
|
2125
|
+
transportInterface: "cli",
|
|
2126
|
+
});
|
|
2127
|
+
|
|
2128
|
+
const client = getCdpClient(ctx);
|
|
2129
|
+
|
|
2130
|
+
expect(client.kind).toBe("extension");
|
|
2131
|
+
const result = await client.send<{ ok: boolean; via: string }>(
|
|
2132
|
+
"Page.navigate",
|
|
2133
|
+
);
|
|
2134
|
+
expect(result).toEqual({ ok: true, via: "extension" });
|
|
2135
|
+
});
|
|
2136
|
+
});
|
|
@@ -1,10 +1,30 @@
|
|
|
1
1
|
import type { HostBrowserProxy } from "../../../daemon/host-browser-proxy.js";
|
|
2
2
|
import { getLogger } from "../../../util/logger.js";
|
|
3
|
+
import type { CdpErrorCode } from "./errors.js";
|
|
3
4
|
import { CdpError } from "./errors.js";
|
|
4
5
|
import type { CdpClientKind, ScopedCdpClient } from "./types.js";
|
|
5
6
|
|
|
6
7
|
const log = getLogger("extension-cdp-client");
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Transport-level error codes that the host_browser dispatcher may
|
|
11
|
+
* embed in a structured `{ code, message }` error envelope. When the
|
|
12
|
+
* `code` field of a parsed error object matches one of these values,
|
|
13
|
+
* the error is classified as `transport_error` so the factory's
|
|
14
|
+
* failover logic can try the next backend candidate.
|
|
15
|
+
*
|
|
16
|
+
* Codes that are NOT in this set are treated as CDP command-level
|
|
17
|
+
* failures (`cdp_error`) and propagate without failover.
|
|
18
|
+
*/
|
|
19
|
+
const TRANSPORT_ERROR_CODES = new Set([
|
|
20
|
+
"transport_error",
|
|
21
|
+
"unreachable",
|
|
22
|
+
"timeout",
|
|
23
|
+
"non_loopback",
|
|
24
|
+
"cdp_session_not_found",
|
|
25
|
+
"cancelled",
|
|
26
|
+
]);
|
|
27
|
+
|
|
8
28
|
/**
|
|
9
29
|
* CdpClient backed by HostBrowserProxy. Each `send` becomes a
|
|
10
30
|
* host_browser_request / host_browser_result round-trip over the
|
|
@@ -102,11 +122,21 @@ export class ExtensionCdpClient implements ScopedCdpClient {
|
|
|
102
122
|
typeof (parsedError as { message: unknown }).message === "string" &&
|
|
103
123
|
(parsedError as { message: string }).message) ||
|
|
104
124
|
`CDP error for ${method}`;
|
|
125
|
+
|
|
126
|
+
// Detect structured transport error envelopes from the
|
|
127
|
+
// host_browser dispatcher. When the parsed error object
|
|
128
|
+
// carries a `code` field that matches a known transport-level
|
|
129
|
+
// code, classify the error as `transport_error` so the
|
|
130
|
+
// factory can trigger failover to the next backend candidate.
|
|
131
|
+
// All other structured errors remain `cdp_error` since they
|
|
132
|
+
// represent command-level CDP failures that would not benefit
|
|
133
|
+
// from switching transports.
|
|
134
|
+
const errorCode = classifyHostBrowserError(parsedError);
|
|
105
135
|
log.debug(
|
|
106
|
-
{ method, params, parsedError },
|
|
107
|
-
"ExtensionCdpClient:
|
|
136
|
+
{ method, params, parsedError, classifiedAs: errorCode },
|
|
137
|
+
"ExtensionCdpClient: host_browser_result error",
|
|
108
138
|
);
|
|
109
|
-
throw new CdpError(
|
|
139
|
+
throw new CdpError(errorCode, msg, {
|
|
110
140
|
cdpMethod: method,
|
|
111
141
|
cdpParams: params,
|
|
112
142
|
underlying: parsedError,
|
|
@@ -139,6 +169,27 @@ export class ExtensionCdpClient implements ScopedCdpClient {
|
|
|
139
169
|
}
|
|
140
170
|
}
|
|
141
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Classify a parsed host_browser_result error envelope as either a
|
|
174
|
+
* transport-level error (`transport_error`) or a command-level CDP
|
|
175
|
+
* failure (`cdp_error`).
|
|
176
|
+
*
|
|
177
|
+
* Structured envelopes from the host_browser dispatcher carry a
|
|
178
|
+
* `code` string field (e.g. `"transport_error"`, `"unreachable"`,
|
|
179
|
+
* `"timeout"`, `"non_loopback"`, `"cdp_session_not_found"`,
|
|
180
|
+
* `"cancelled"`). When the code matches a known
|
|
181
|
+
* transport-level value, the error is eligible for factory failover.
|
|
182
|
+
* All other codes (or missing codes) are treated as CDP command
|
|
183
|
+
* errors that should propagate without failover.
|
|
184
|
+
*/
|
|
185
|
+
function classifyHostBrowserError(parsed: unknown): CdpErrorCode {
|
|
186
|
+
if (typeof parsed !== "object" || parsed === null) return "cdp_error";
|
|
187
|
+
if (!("code" in parsed)) return "cdp_error";
|
|
188
|
+
const code = (parsed as { code: unknown }).code;
|
|
189
|
+
if (typeof code !== "string") return "cdp_error";
|
|
190
|
+
return TRANSPORT_ERROR_CODES.has(code) ? "transport_error" : "cdp_error";
|
|
191
|
+
}
|
|
192
|
+
|
|
142
193
|
export function createExtensionCdpClient(
|
|
143
194
|
proxy: HostBrowserProxy,
|
|
144
195
|
conversationId: string,
|
|
@@ -349,15 +349,26 @@ export function buildCandidateList(context: ToolContext): BackendCandidate[] {
|
|
|
349
349
|
cdpInspectConfig.desktopAuto.enabled
|
|
350
350
|
) {
|
|
351
351
|
// macOS desktop-auto: include cdp-inspect as a candidate unless:
|
|
352
|
-
// (a) the hostBrowserProxy
|
|
353
|
-
//
|
|
354
|
-
//
|
|
352
|
+
// (a) the hostBrowserProxy is registry-routed (extension-backed) and
|
|
353
|
+
// temporarily unavailable — the extension transport was explicitly
|
|
354
|
+
// expected and the disconnection is transient, so inserting
|
|
355
|
+
// cdp-inspect would cause a silent takeover. Only applies when
|
|
356
|
+
// `hostBrowserRegistryRouted` is true (set when
|
|
357
|
+
// `hostBrowserSenderOverride` was wired at turn-start).
|
|
358
|
+
// SSE-backed proxies (macOS without an extension connection) that
|
|
359
|
+
// report unavailable (e.g. non-interactive turns where
|
|
360
|
+
// clientConnected=false) should NOT suppress cdp-inspect — the
|
|
361
|
+
// SSE proxy was never expected to service browser requests.
|
|
355
362
|
// (b) the cooldown from a recent failure is still active.
|
|
356
363
|
//
|
|
357
364
|
// When no hostBrowserProxy is present at all (extension not
|
|
358
365
|
// provisioned for this conversation), cdp-inspect remains available
|
|
359
366
|
// as a fallback per the desktop-auto contract.
|
|
360
|
-
if (
|
|
367
|
+
if (
|
|
368
|
+
hostBrowserProxy &&
|
|
369
|
+
!hostBrowserProxy.isAvailable() &&
|
|
370
|
+
context.hostBrowserRegistryRouted
|
|
371
|
+
) {
|
|
361
372
|
log.debug(
|
|
362
373
|
{ conversationId },
|
|
363
374
|
"CDP factory: desktop-auto cdp-inspect skipped (extension transport expected but temporarily unavailable)",
|
package/src/tools/executor.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
|
|
3
|
+
import { parseChannelId } from "../channels/types.js";
|
|
3
4
|
import { getConfig } from "../config/loader.js";
|
|
4
5
|
import { bridgeCesApproval } from "../credential-execution/approval-bridge.js";
|
|
5
6
|
import { isCesShellLockdownEnabled } from "../credential-execution/feature-gates.js";
|
|
6
|
-
import { getHookManager } from "../hooks/manager.js";
|
|
7
7
|
import { PermissionPrompter } from "../permissions/prompter.js";
|
|
8
8
|
import { RiskLevel } from "../permissions/types.js";
|
|
9
|
+
import { runPipeline } from "../plugins/pipeline.js";
|
|
10
|
+
import { getMiddlewaresFor } from "../plugins/registry.js";
|
|
11
|
+
import type {
|
|
12
|
+
ToolExecuteArgs,
|
|
13
|
+
ToolExecuteResult,
|
|
14
|
+
TurnContext,
|
|
15
|
+
} from "../plugins/types.js";
|
|
9
16
|
import { isUntrustedTrustClass } from "../runtime/actor-trust-resolver.js";
|
|
10
17
|
import { redactSensitiveFields } from "../security/redaction.js";
|
|
11
18
|
import { TokenExpiredError } from "../security/token-manager.js";
|
|
@@ -46,6 +53,59 @@ export class ToolExecutor {
|
|
|
46
53
|
name: string,
|
|
47
54
|
input: Record<string, unknown>,
|
|
48
55
|
context: ToolContext,
|
|
56
|
+
/**
|
|
57
|
+
* Optional per-turn context threaded in by the agent loop. Production
|
|
58
|
+
* sites propagate the orchestrator-built `TurnContext` (real
|
|
59
|
+
* `conversationId`, trust cascade, attached `contextWindowManager`) so
|
|
60
|
+
* middleware registered on the `toolExecute` pipeline sees the same
|
|
61
|
+
* context every other pipeline slot uses. When omitted (CLI/test
|
|
62
|
+
* invocations that call `ToolExecutor.execute` directly), the executor
|
|
63
|
+
* synthesizes a fallback context from the {@link ToolContext}, which
|
|
64
|
+
* keeps pre-threading behavior intact for legacy callers.
|
|
65
|
+
*/
|
|
66
|
+
turnContext?: TurnContext,
|
|
67
|
+
): Promise<ToolExecutionResult> {
|
|
68
|
+
// Prefer the orchestrator-supplied `turnContext` so the pipeline sees
|
|
69
|
+
// the real conversation identity, per-turn trust, and context-window
|
|
70
|
+
// manager. When absent (CLI / test invocations that bypass the agent
|
|
71
|
+
// loop), synthesize a minimal context from the `ToolContext` — the
|
|
72
|
+
// same fallback the executor has used since the pipeline was added.
|
|
73
|
+
const turnCtx: TurnContext = turnContext ?? {
|
|
74
|
+
requestId: context.requestId ?? "",
|
|
75
|
+
conversationId: context.conversationId,
|
|
76
|
+
turnIndex: 0,
|
|
77
|
+
trust: {
|
|
78
|
+
sourceChannel: parseChannelId(context.executionChannel) ?? "vellum",
|
|
79
|
+
trustClass: context.trustClass,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const middlewares = getMiddlewaresFor("toolExecute");
|
|
84
|
+
const pipelineArgs: ToolExecuteArgs = { name, input, context };
|
|
85
|
+
|
|
86
|
+
// No pipeline-level timeout: `executeInternal` already wraps the real
|
|
87
|
+
// tool invocation in `executeWithTimeout`, which is the sole enforcer
|
|
88
|
+
// of the per-tool budget. Propagating `perToolTimeoutMs` to
|
|
89
|
+
// `runPipeline` made the pipeline race everything upstream of the
|
|
90
|
+
// tool call — permission checks, approval waits, middleware — against
|
|
91
|
+
// the same budget, so a slow human clicking "allow" produced a
|
|
92
|
+
// `PluginTimeoutError` thrown past `executeInternal`'s catch block,
|
|
93
|
+
// breaking the `execute()` never-throws contract. Letting the pipeline
|
|
94
|
+
// run untimed keeps the contract intact; runaway middleware is a
|
|
95
|
+
// plugin-health concern handled by per-plugin timeouts, not here.
|
|
96
|
+
return runPipeline<ToolExecuteArgs, ToolExecuteResult>(
|
|
97
|
+
"toolExecute",
|
|
98
|
+
middlewares,
|
|
99
|
+
(args) => this.executeInternal(args.name, args.input, args.context),
|
|
100
|
+
pipelineArgs,
|
|
101
|
+
turnCtx,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private async executeInternal(
|
|
106
|
+
name: string,
|
|
107
|
+
input: Record<string, unknown>,
|
|
108
|
+
context: ToolContext,
|
|
49
109
|
): Promise<ToolExecutionResult> {
|
|
50
110
|
const startTime = Date.now();
|
|
51
111
|
let decision = "allow";
|
|
@@ -112,6 +172,14 @@ export class ToolExecutor {
|
|
|
112
172
|
// Exception: requireFreshApproval tools always go through the
|
|
113
173
|
// permission check even when a grant was consumed - the grant does
|
|
114
174
|
// not substitute for an interactive human review.
|
|
175
|
+
let permRiskMeta:
|
|
176
|
+
| {
|
|
177
|
+
riskLevel: string;
|
|
178
|
+
riskReason: string;
|
|
179
|
+
riskScopeOptions: Array<{ pattern: string; label: string }>;
|
|
180
|
+
isContainerized?: boolean;
|
|
181
|
+
}
|
|
182
|
+
| undefined;
|
|
115
183
|
if (!gateResult.grantConsumed || context.requireFreshApproval) {
|
|
116
184
|
// Check permissions via the extracted PermissionChecker
|
|
117
185
|
const permResult = await this.permissionChecker.checkPermission(
|
|
@@ -121,16 +189,23 @@ export class ToolExecutor {
|
|
|
121
189
|
context,
|
|
122
190
|
executionTarget,
|
|
123
191
|
(event) => emitLifecycleEvent(context, event),
|
|
124
|
-
sanitizeToolInput,
|
|
125
192
|
startTime,
|
|
126
193
|
computePreviewDiff,
|
|
127
194
|
);
|
|
128
195
|
|
|
129
196
|
riskLevel = permResult.riskLevel;
|
|
130
197
|
decision = permResult.decision;
|
|
198
|
+
permRiskMeta = permResult.riskMeta;
|
|
131
199
|
|
|
132
200
|
if (!permResult.allowed) {
|
|
133
|
-
return {
|
|
201
|
+
return {
|
|
202
|
+
content: permResult.content,
|
|
203
|
+
isError: true,
|
|
204
|
+
riskLevel: permRiskMeta?.riskLevel,
|
|
205
|
+
riskReason: permRiskMeta?.riskReason,
|
|
206
|
+
riskScopeOptions: permRiskMeta?.riskScopeOptions,
|
|
207
|
+
isContainerized: permRiskMeta?.isContainerized,
|
|
208
|
+
};
|
|
134
209
|
}
|
|
135
210
|
|
|
136
211
|
if (permResult.wasPrompted) {
|
|
@@ -138,59 +213,11 @@ export class ToolExecutor {
|
|
|
138
213
|
}
|
|
139
214
|
}
|
|
140
215
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
riskLevel,
|
|
145
|
-
decision,
|
|
146
|
-
workingDir: context.workingDir,
|
|
147
|
-
conversationId: context.conversationId,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
if (hookResult.blocked) {
|
|
151
|
-
const msg = `Tool execution blocked by hook "${hookResult.blockedBy}"`;
|
|
152
|
-
const durationMs = Date.now() - startTime;
|
|
153
|
-
emitLifecycleEvent(context, {
|
|
154
|
-
type: "error",
|
|
155
|
-
toolName: name,
|
|
156
|
-
executionTarget,
|
|
157
|
-
input,
|
|
158
|
-
workingDir: context.workingDir,
|
|
159
|
-
conversationId: context.conversationId,
|
|
160
|
-
requestId: context.requestId,
|
|
161
|
-
riskLevel,
|
|
162
|
-
decision: "blocked",
|
|
163
|
-
durationMs,
|
|
164
|
-
errorMessage: msg,
|
|
165
|
-
isExpected: true,
|
|
166
|
-
errorCategory: "tool_failure",
|
|
167
|
-
});
|
|
168
|
-
return { content: msg, isError: true };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Execute the tool - proxy tools delegate to an external resolver
|
|
216
|
+
// Execute the tool - proxy tools delegate to an external resolver.
|
|
217
|
+
// Use the shared per-tool timeout helper so the pipeline runner and
|
|
218
|
+
// the inner execute-with-timeout wrapper agree on the same budget.
|
|
172
219
|
let execResult: ToolExecutionResult;
|
|
173
|
-
|
|
174
|
-
if (name === "bash" || name === "host_bash") {
|
|
175
|
-
// Shell tools manage their own timeouts (SIGKILL on expiry).
|
|
176
|
-
// Compute the same effective timeout so the executor wrapper
|
|
177
|
-
// doesn't prematurely kill them with the generic toolExecutionTimeoutSec.
|
|
178
|
-
const { shellDefaultTimeoutSec, shellMaxTimeoutSec } =
|
|
179
|
-
getConfig().timeouts;
|
|
180
|
-
const requestedSec =
|
|
181
|
-
typeof input.timeout_seconds === "number"
|
|
182
|
-
? input.timeout_seconds
|
|
183
|
-
: shellDefaultTimeoutSec;
|
|
184
|
-
const shellTimeoutSec = Math.max(
|
|
185
|
-
1,
|
|
186
|
-
Math.min(requestedSec, shellMaxTimeoutSec),
|
|
187
|
-
);
|
|
188
|
-
// Buffer so the shell's own timeout fires first and handles cleanup
|
|
189
|
-
toolTimeoutMs = (shellTimeoutSec + 5) * 1000;
|
|
190
|
-
} else {
|
|
191
|
-
const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
|
|
192
|
-
toolTimeoutMs = safeTimeoutMs(rawTimeoutSec);
|
|
193
|
-
}
|
|
220
|
+
const toolTimeoutMs = computePerToolTimeoutMs(name, input);
|
|
194
221
|
|
|
195
222
|
const execContext = context;
|
|
196
223
|
|
|
@@ -364,7 +391,6 @@ export class ToolExecutor {
|
|
|
364
391
|
decision,
|
|
365
392
|
startTime,
|
|
366
393
|
emitLifecycleEvent,
|
|
367
|
-
sanitizeToolInput,
|
|
368
394
|
);
|
|
369
395
|
if (secretResult.earlyReturn) {
|
|
370
396
|
return secretResult.result;
|
|
@@ -388,14 +414,18 @@ export class ToolExecutor {
|
|
|
388
414
|
result: safeResult,
|
|
389
415
|
});
|
|
390
416
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
417
|
+
// Merge risk metadata from the classifier assessment cache onto the
|
|
418
|
+
// tool result so downstream consumers (AgentEvent → handleToolResult →
|
|
419
|
+
// ToolResult SSE message) can forward it to the client.
|
|
420
|
+
if (permRiskMeta) {
|
|
421
|
+
execResult = {
|
|
422
|
+
...execResult,
|
|
423
|
+
riskLevel: permRiskMeta.riskLevel,
|
|
424
|
+
riskReason: permRiskMeta.riskReason,
|
|
425
|
+
riskScopeOptions: permRiskMeta.riskScopeOptions,
|
|
426
|
+
isContainerized: permRiskMeta.isContainerized,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
399
429
|
|
|
400
430
|
return execResult;
|
|
401
431
|
} catch (err) {
|
|
@@ -446,15 +476,6 @@ export class ToolExecutor {
|
|
|
446
476
|
errorStack: err instanceof Error ? err.stack : undefined,
|
|
447
477
|
});
|
|
448
478
|
|
|
449
|
-
void getHookManager().trigger("post-tool-execute", {
|
|
450
|
-
toolName: name,
|
|
451
|
-
input: sanitizeToolInput(name, input),
|
|
452
|
-
riskLevel,
|
|
453
|
-
isError: true,
|
|
454
|
-
durationMs,
|
|
455
|
-
conversationId: context.conversationId,
|
|
456
|
-
});
|
|
457
|
-
|
|
458
479
|
if (isExpected) {
|
|
459
480
|
return { content: msg, isError: true };
|
|
460
481
|
}
|
|
@@ -474,7 +495,38 @@ export { isSideEffectTool } from "./side-effects.js";
|
|
|
474
495
|
export { PermissionChecker } from "./permission-checker.js";
|
|
475
496
|
|
|
476
497
|
/**
|
|
477
|
-
*
|
|
498
|
+
* Compute the effective per-tool execution timeout in milliseconds.
|
|
499
|
+
*
|
|
500
|
+
* Shell tools (`bash`, `host_bash`) manage their own timeouts with SIGKILL
|
|
501
|
+
* on expiry. We add a 5s buffer so the shell's own deadline fires first and
|
|
502
|
+
* handles cleanup before the executor wrapper trips. Non-shell tools use
|
|
503
|
+
* the generic `toolExecutionTimeoutSec` configuration value.
|
|
504
|
+
*
|
|
505
|
+
* Consumed by `executeInternal` via `executeWithTimeout`, which is the
|
|
506
|
+
* sole enforcer of the per-tool budget.
|
|
507
|
+
*/
|
|
508
|
+
function computePerToolTimeoutMs(
|
|
509
|
+
name: string,
|
|
510
|
+
input: Record<string, unknown>,
|
|
511
|
+
): number {
|
|
512
|
+
if (name === "bash" || name === "host_bash") {
|
|
513
|
+
const { shellDefaultTimeoutSec, shellMaxTimeoutSec } = getConfig().timeouts;
|
|
514
|
+
const requestedSec =
|
|
515
|
+
typeof input.timeout_seconds === "number"
|
|
516
|
+
? input.timeout_seconds
|
|
517
|
+
: shellDefaultTimeoutSec;
|
|
518
|
+
const shellTimeoutSec = Math.max(
|
|
519
|
+
1,
|
|
520
|
+
Math.min(requestedSec, shellMaxTimeoutSec),
|
|
521
|
+
);
|
|
522
|
+
return (shellTimeoutSec + 5) * 1000;
|
|
523
|
+
}
|
|
524
|
+
const rawTimeoutSec = getConfig().timeouts.toolExecutionTimeoutSec;
|
|
525
|
+
return safeTimeoutMs(rawTimeoutSec);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Sanitize tool inputs before they are emitted in lifecycle events.
|
|
478
530
|
* Applies recursive field-level redaction for known-sensitive keys.
|
|
479
531
|
*/
|
|
480
532
|
function sanitizeToolInput(
|
|
@@ -81,6 +81,36 @@ const ALLOWED_HOST_PATTERNS: readonly string[] = (() => {
|
|
|
81
81
|
return defaults;
|
|
82
82
|
})();
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Non-sensitive HTTP request headers that are safe to surface in the
|
|
86
|
+
* `network_request` approval prompt. Strict allowlist to keep Authorization,
|
|
87
|
+
* Cookie, X-Api-Key, and other custom credential-bearing headers off-screen.
|
|
88
|
+
*/
|
|
89
|
+
const APPROVAL_HEADER_ALLOWLIST: readonly string[] = [
|
|
90
|
+
"content-type",
|
|
91
|
+
"content-length",
|
|
92
|
+
"user-agent",
|
|
93
|
+
"accept",
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Project an incoming header map onto {@link APPROVAL_HEADER_ALLOWLIST},
|
|
98
|
+
* collapsing multi-value arrays to a comma-joined string. Returns undefined
|
|
99
|
+
* when no headers are available (e.g. HTTPS CONNECT path).
|
|
100
|
+
*/
|
|
101
|
+
function filterApprovalHeaders(
|
|
102
|
+
raw: Record<string, string | string[] | undefined> | undefined,
|
|
103
|
+
): Record<string, string> | undefined {
|
|
104
|
+
if (!raw) return undefined;
|
|
105
|
+
const out: Record<string, string> = {};
|
|
106
|
+
for (const key of APPROVAL_HEADER_ALLOWLIST) {
|
|
107
|
+
const value = raw[key];
|
|
108
|
+
if (value === undefined) continue;
|
|
109
|
+
out[key] = Array.isArray(value) ? value.join(", ") : value;
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
|
|
84
114
|
/**
|
|
85
115
|
* Returns `true` when `hostname` matches any entry in
|
|
86
116
|
* {@link ALLOWED_HOST_PATTERNS}.
|
|
@@ -292,12 +322,16 @@ function buildSessionStartHooks(): SessionStartHooks {
|
|
|
292
322
|
return allKnownCache;
|
|
293
323
|
}
|
|
294
324
|
|
|
295
|
-
// Build the policy callback for HTTP/CONNECT request gating
|
|
325
|
+
// Build the policy callback for HTTP/CONNECT request gating.
|
|
326
|
+
// `method` / `reqHeaders` are populated for plain-HTTP proxied requests
|
|
327
|
+
// and undefined for HTTPS CONNECT tunnels (TLS not yet terminated).
|
|
296
328
|
const policyCallback: PolicyCallback = async (
|
|
297
329
|
hostname: string,
|
|
298
330
|
port: number | null,
|
|
299
331
|
reqPath: string,
|
|
300
332
|
scheme: "http" | "https",
|
|
333
|
+
method?: string,
|
|
334
|
+
reqHeaders?: Record<string, string | string[] | undefined>,
|
|
301
335
|
) => {
|
|
302
336
|
if (isAllowedHost(hostname)) {
|
|
303
337
|
log.debug({ hostname }, "Allowing always-permitted host");
|
|
@@ -356,6 +390,8 @@ function buildSessionStartHooks(): SessionStartHooks {
|
|
|
356
390
|
const approved = await managed.approvalCallback({
|
|
357
391
|
decision,
|
|
358
392
|
sessionId: managed.session.id,
|
|
393
|
+
method,
|
|
394
|
+
requestHeaders: filterApprovalHeaders(reqHeaders),
|
|
359
395
|
});
|
|
360
396
|
return approved ? {} : null;
|
|
361
397
|
}
|