@vellumai/assistant 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bun.lock +40 -40
- package/bunfig.toml +3 -0
- package/docs/architecture/memory.md +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +42 -0
- package/openapi.yaml +184 -69
- package/package.json +41 -41
- package/scripts/generate-openapi.ts +1 -2
- package/src/__tests__/acp-session.test.ts +43 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +1 -0
- package/src/__tests__/app-source-watcher.test.ts +37 -11
- package/src/__tests__/approval-routes-http.test.ts +178 -1
- package/src/__tests__/browser-fill-credential.test.ts +229 -94
- package/src/__tests__/browser-manager.test.ts +40 -27
- package/src/__tests__/catalog-files.test.ts +862 -0
- package/src/__tests__/channel-approvals.test.ts +53 -0
- package/src/__tests__/config-managed-gemini-defaults.test.ts +326 -0
- package/src/__tests__/config-schema-cmd.test.ts +2 -2
- package/src/__tests__/config-schema.test.ts +125 -48
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +23 -0
- package/src/__tests__/context-overflow-approval.test.ts +16 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -1
- package/src/__tests__/conversation-agent-loop.test.ts +1 -1
- package/src/__tests__/conversation-analysis-routes.test.ts +2 -2
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
- package/src/__tests__/conversation-fork-crud.test.ts +17 -0
- package/src/__tests__/conversation-history-web-search.test.ts +1 -0
- package/src/__tests__/conversation-host-access-routes.test.ts +229 -0
- package/src/__tests__/conversation-inject-context.test.ts +103 -0
- package/src/__tests__/conversation-queue.test.ts +45 -2
- package/src/__tests__/conversation-routes-disk-view.test.ts +5 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +16 -0
- package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +269 -46
- package/src/__tests__/conversation-starter-routes.test.ts +126 -0
- package/src/__tests__/conversation-starters-cadence.test.ts +161 -0
- package/src/__tests__/conversation-store.test.ts +195 -0
- package/src/__tests__/conversation-workspace-cache-state.test.ts +193 -0
- package/src/__tests__/credential-execution-approval-bridge.test.ts +32 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/credential-vault-unit.test.ts +4 -4
- package/src/__tests__/credential-vault.test.ts +152 -13
- package/src/__tests__/credentials-cli.test.ts +2 -2
- package/src/__tests__/date-context.test.ts +4 -4
- package/src/__tests__/embedding-managed-proxy-selection.test.ts +256 -0
- package/src/__tests__/extension-id-sync-guard.test.ts +155 -0
- package/src/__tests__/fixtures/mock-chrome-extension.ts +375 -0
- package/src/__tests__/gateway-only-guard.test.ts +3 -0
- package/src/__tests__/gemini-provider.test.ts +2 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +70 -2
- package/src/__tests__/headless-browser-interactions.test.ts +707 -371
- package/src/__tests__/headless-browser-navigate.test.ts +389 -47
- package/src/__tests__/headless-browser-read-tools.test.ts +266 -103
- package/src/__tests__/headless-browser-snapshot.test.ts +240 -77
- package/src/__tests__/host-bash-proxy.test.ts +150 -1
- package/src/__tests__/host-browser-e2e-cloud.test.ts +462 -0
- package/src/__tests__/host-browser-e2e-self-hosted-capability.test.ts +286 -0
- package/src/__tests__/host-browser-e2e-self-hosted.test.ts +374 -0
- package/src/__tests__/host-browser-event-routes.test.ts +350 -0
- package/src/__tests__/host-browser-proxy.test.ts +444 -0
- package/src/__tests__/host-browser-routes.test.ts +198 -0
- package/src/__tests__/host-browser-ws-events-e2e.test.ts +320 -0
- package/src/__tests__/host-cu-proxy.test.ts +171 -1
- package/src/__tests__/host-file-proxy.test.ts +185 -1
- package/src/__tests__/host-file-read-tool.test.ts +52 -0
- package/src/__tests__/host-proxy-interface.test.ts +165 -0
- package/src/__tests__/host-shell-tool.test.ts +1 -11
- package/src/__tests__/http-user-message-parity.test.ts +1 -0
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- package/src/__tests__/mcp-client-auth.test.ts +40 -4
- package/src/__tests__/mcp-health-check.test.ts +10 -3
- package/src/__tests__/migration-cross-version-compatibility.test.ts +3 -1
- package/src/__tests__/migration-export-http.test.ts +61 -2
- package/src/__tests__/migration-export-streaming.test.ts +66 -0
- package/src/__tests__/migration-import-commit-http.test.ts +101 -1
- package/src/__tests__/native-host-marker-sync-guard.test.ts +157 -0
- package/src/__tests__/oauth-apps-routes.test.ts +17 -12
- package/src/__tests__/oauth-cli.test.ts +707 -60
- package/src/__tests__/oauth-connect-orchestrator.test.ts +116 -24
- package/src/__tests__/oauth-provider-seed-logos.test.ts +23 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +146 -10
- package/src/__tests__/oauth-provider-visibility.test.ts +19 -21
- package/src/__tests__/oauth-providers-routes.test.ts +50 -14
- package/src/__tests__/oauth-store.test.ts +1386 -182
- package/src/__tests__/oauth2-gateway-transport.test.ts +211 -20
- package/src/__tests__/onboarding-template-contract.test.ts +75 -57
- package/src/__tests__/openai-provider.test.ts +2 -2
- package/src/__tests__/outlook-categories.test.ts +1 -1
- package/src/__tests__/outlook-client-automation.test.ts +1 -1
- package/src/__tests__/outlook-compose-tools.test.ts +1 -1
- package/src/__tests__/outlook-email-watcher.test.ts +1 -1
- package/src/__tests__/outlook-follow-up.test.ts +1 -1
- package/src/__tests__/outlook-messaging-provider.test.ts +2 -2
- package/src/__tests__/outlook-trash.test.ts +1 -1
- package/src/__tests__/outlook-unsubscribe.test.ts +1 -1
- package/src/__tests__/permission-checker-host-gate.test.ts +74 -14
- package/src/__tests__/permission-mode.test.ts +28 -56
- package/src/__tests__/platform-callback-registration.test.ts +19 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +296 -0
- package/src/__tests__/proxy-approval-callback.test.ts +18 -0
- package/src/__tests__/require-fresh-approval.test.ts +40 -1
- package/src/__tests__/sanitize-config-for-transfer.test.ts +132 -0
- package/src/__tests__/schedule-routes.test.ts +162 -0
- package/src/__tests__/secret-detection-handler.test.ts +84 -0
- package/src/__tests__/secret-ingress-http.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +3 -0
- package/src/__tests__/set-permission-mode.test.ts +13 -250
- package/src/__tests__/skills-file-content-endpoint.test.ts +670 -0
- package/src/__tests__/skills-files-catalog-fallback.test.ts +450 -0
- package/src/__tests__/slack-channel-config.test.ts +12 -15
- package/src/__tests__/subagent-detail.test.ts +44 -2
- package/src/__tests__/subagent-disposal.test.ts +1 -0
- package/src/__tests__/subagent-fork-notifications.test.ts +291 -0
- package/src/__tests__/subagent-fork-spawn.test.ts +384 -0
- package/src/__tests__/subagent-manager-notify.test.ts +1 -0
- package/src/__tests__/subagent-notify-parent.test.ts +1 -0
- package/src/__tests__/subagent-spawn-tool-fork.test.ts +411 -0
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/subagent-types.test.ts +1 -0
- package/src/__tests__/system-prompt-ask-mode.test.ts +27 -71
- package/src/__tests__/system-prompt.test.ts +72 -1
- package/src/__tests__/task-scheduler.test.ts +32 -6
- package/src/__tests__/telegram-config.test.ts +10 -13
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +22 -0
- package/src/__tests__/top-level-renderer.test.ts +73 -1
- package/src/__tests__/transport-hints-queue.test.ts +14 -29
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- package/src/__tests__/v2-consent-policy.test.ts +103 -0
- package/src/acp/client-handler.ts +30 -4
- package/src/agent/loop.ts +12 -6
- package/src/approvals/guardian-request-resolvers.ts +21 -15
- package/src/browser-session/__tests__/manager.test.ts +297 -0
- package/src/browser-session/backends/cdp-inspect.ts +30 -0
- package/src/browser-session/backends/extension.ts +26 -0
- package/src/browser-session/backends/local.ts +24 -0
- package/src/browser-session/events.ts +164 -0
- package/src/browser-session/index.ts +27 -0
- package/src/browser-session/manager.ts +159 -0
- package/src/browser-session/types.ts +28 -0
- package/src/channels/__tests__/types.test.ts +134 -0
- package/src/channels/types.ts +53 -3
- package/src/cli/commands/browser-relay.ts +339 -409
- package/src/cli/commands/credentials.ts +3 -3
- package/src/cli/commands/email.ts +18 -13
- package/src/cli/commands/mcp.ts +16 -4
- package/src/cli/commands/oauth/__tests__/connect.test.ts +44 -44
- package/src/cli/commands/oauth/__tests__/disconnect.test.ts +21 -21
- package/src/cli/commands/oauth/__tests__/mode.test.ts +17 -17
- package/src/cli/commands/oauth/__tests__/ping.test.ts +16 -16
- package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +31 -33
- package/src/cli/commands/oauth/__tests__/providers-register.test.ts +329 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +116 -12
- package/src/cli/commands/oauth/__tests__/status.test.ts +10 -10
- package/src/cli/commands/oauth/__tests__/token.test.ts +7 -7
- package/src/cli/commands/oauth/apps.ts +7 -4
- package/src/cli/commands/oauth/connect.ts +6 -3
- package/src/cli/commands/oauth/disconnect.ts +1 -1
- package/src/cli/commands/oauth/providers.ts +200 -36
- package/src/cli/commands/oauth/shared.ts +5 -5
- package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +259 -0
- package/src/cli/commands/platform/index.ts +107 -10
- package/src/cli/commands/usage.ts +10 -9
- package/src/cli/lib/daemon-credential-client.ts +4 -0
- package/src/cli/program.ts +1 -1
- package/src/config/bundled-skills/app-builder/SKILL.md +26 -249
- package/src/config/bundled-skills/app-builder/references/CUSTOM_ROUTES.md +105 -0
- package/src/config/bundled-skills/app-builder/references/INTERACTION_HOOKS.md +56 -0
- package/src/config/bundled-skills/app-builder/references/WIDGETS.md +125 -0
- package/src/config/bundled-skills/contacts/SKILL.md +3 -0
- package/src/config/bundled-skills/document/SKILL.md +4 -0
- package/src/config/bundled-skills/gmail/SKILL.md +1 -1
- package/src/config/bundled-skills/outlook/SKILL.md +7 -0
- package/src/config/bundled-skills/subagent/SKILL.md +21 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -4
- package/src/config/bundled-skills/tasks/SKILL.md +5 -0
- package/src/config/env-registry.ts +14 -0
- package/src/config/env.ts +21 -0
- package/src/config/feature-flag-registry.json +44 -5
- package/src/config/loader.ts +56 -1
- package/src/config/sanitize-for-transfer.ts +47 -0
- package/src/config/schema.ts +46 -5
- package/src/config/schemas/host-browser.ts +66 -0
- package/src/config/schemas/memory-lifecycle.ts +1 -1
- package/src/config/schemas/memory-retrieval.ts +103 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +8 -0
- package/src/config/types.ts +0 -1
- package/src/context/post-turn-tool-result-truncation.ts +176 -0
- package/src/context/window-manager.ts +19 -1
- package/src/credential-execution/approval-bridge.ts +49 -15
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/context-overflow-approval.ts +5 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
- package/src/daemon/conversation-agent-loop.ts +58 -24
- package/src/daemon/conversation-attachments.ts +40 -0
- package/src/daemon/conversation-process.ts +48 -1
- package/src/daemon/conversation-runtime-assembly.ts +118 -36
- package/src/daemon/conversation-surfaces.ts +37 -36
- package/src/daemon/conversation-tool-setup.ts +74 -8
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +226 -8
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/conversations.ts +9 -140
- package/src/daemon/handlers/shared.ts +58 -0
- package/src/daemon/handlers/skills.ts +232 -37
- package/src/daemon/host-bash-proxy.ts +48 -13
- package/src/daemon/host-browser-proxy.ts +191 -0
- package/src/daemon/host-cu-proxy.ts +36 -11
- package/src/daemon/host-file-proxy.ts +57 -9
- package/src/daemon/lifecycle.ts +65 -11
- package/src/daemon/message-protocol.ts +7 -0
- package/src/daemon/message-types/conversations.ts +55 -13
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/messages.ts +5 -5
- package/src/daemon/message-types/skills.ts +10 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/server.ts +92 -12
- package/src/daemon/tool-side-effects.ts +6 -0
- package/src/daemon/transport-hints.ts +5 -24
- package/src/inbound/platform-callback-registration.ts +18 -17
- package/src/mcp/client.ts +59 -24
- package/src/memory/app-store.ts +31 -1
- package/src/memory/conversation-crud.ts +23 -0
- package/src/memory/conversation-starters-cadence.ts +76 -0
- package/src/memory/conversation-title-service.ts +5 -2
- package/src/memory/db-init.ts +12 -0
- package/src/memory/embedding-backend.test.ts +75 -0
- package/src/memory/embedding-backend.ts +131 -5
- package/src/memory/embedding-gemini.test.ts +54 -0
- package/src/memory/embedding-gemini.ts +20 -9
- package/src/memory/embedding-local.ts +176 -17
- package/src/memory/graph/consolidation.ts +10 -23
- package/src/memory/graph/extraction-job.ts +15 -0
- package/src/memory/graph/retriever.ts +40 -22
- package/src/memory/graph/store.test.ts +7 -3
- package/src/memory/graph/store.ts +47 -12
- package/src/memory/llm-usage-store.ts +45 -4
- package/src/memory/migrations/213-oauth-providers-scope-separator.ts +13 -0
- package/src/memory/migrations/214-oauth-providers-refresh-url.ts +11 -0
- package/src/memory/migrations/215-oauth-providers-revoke.ts +14 -0
- package/src/memory/migrations/216-oauth-providers-token-auth-method.ts +30 -0
- package/src/memory/migrations/217-conversation-host-access.ts +40 -0
- package/src/memory/migrations/218-oauth-providers-logo-url.ts +11 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +1 -0
- package/src/memory/schema/oauth.ts +18 -13
- package/src/oauth/AGENTS.md +76 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +24 -19
- package/src/oauth/__tests__/seed-providers-managed.test.ts +32 -0
- package/src/oauth/byo-connection.test.ts +8 -8
- package/src/oauth/byo-connection.ts +7 -7
- package/src/oauth/connect-orchestrator.ts +23 -21
- package/src/oauth/connect-types.ts +3 -3
- package/src/oauth/connection-resolver.test.ts +17 -4
- package/src/oauth/connection-resolver.ts +16 -16
- package/src/oauth/connection.ts +1 -1
- package/src/oauth/manual-token-connection.ts +13 -13
- package/src/oauth/oauth-store.ts +214 -100
- package/src/oauth/platform-connection.test.ts +3 -3
- package/src/oauth/platform-connection.ts +4 -4
- package/src/oauth/provider-serializer.ts +31 -5
- package/src/oauth/revoke.ts +76 -0
- package/src/oauth/seed-providers.ts +126 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -1
- package/src/permissions/v2-consent-policy.ts +87 -0
- package/src/prompts/system-prompt.ts +18 -21
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -105
- package/src/providers/anthropic/client.ts +1 -0
- package/src/providers/types.ts +1 -1
- package/src/runtime/AGENTS.md +23 -0
- package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +715 -0
- package/src/runtime/__tests__/capability-tokens.test.ts +258 -0
- package/src/runtime/__tests__/chrome-extension-registry.test.ts +518 -0
- package/src/runtime/assistant-event-hub.ts +2 -2
- package/src/runtime/auth/__tests__/guard-tests.test.ts +1 -0
- package/src/runtime/auth/__tests__/middleware.test.ts +116 -1
- package/src/runtime/auth/__tests__/route-policy.test.ts +8 -0
- package/src/runtime/auth/middleware.ts +98 -0
- package/src/runtime/auth/route-policy.ts +6 -7
- package/src/runtime/capability-tokens.ts +414 -0
- package/src/runtime/channel-approvals.ts +18 -5
- package/src/runtime/chrome-extension-registry.ts +332 -0
- package/src/runtime/confirmation-request-guardian-bridge.ts +6 -0
- package/src/runtime/guardian-decision-types.ts +7 -0
- package/src/runtime/http-server.ts +425 -70
- package/src/runtime/migrations/__tests__/rebind-secrets-credentials.test.ts +172 -0
- package/src/runtime/migrations/__tests__/vbundle-builder-credentials.test.ts +276 -0
- package/src/runtime/migrations/__tests__/vbundle-import-credentials.test.ts +162 -0
- package/src/runtime/migrations/migration-transport.ts +6 -0
- package/src/runtime/migrations/migration-wizard.ts +22 -2
- package/src/runtime/migrations/rebind-secrets-screen.ts +76 -15
- package/src/runtime/migrations/vbundle-builder.ts +145 -38
- package/src/runtime/migrations/vbundle-import-analyzer.ts +19 -0
- package/src/runtime/migrations/vbundle-importer.ts +55 -5
- package/src/runtime/pending-interactions.ts +29 -13
- package/src/runtime/routes/approval-routes.ts +90 -16
- package/src/runtime/routes/browser-cdp-routes.ts +229 -0
- package/src/runtime/routes/browser-extension-pair-routes.ts +497 -0
- package/src/runtime/routes/conversation-analysis-routes.ts +2 -1
- package/src/runtime/routes/conversation-management-routes.ts +108 -0
- package/src/runtime/routes/conversation-routes.ts +301 -27
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/guardian-action-routes.ts +24 -13
- package/src/runtime/routes/host-browser-routes.ts +279 -0
- package/src/runtime/routes/host-file-routes.ts +9 -1
- package/src/runtime/routes/identity-routes.ts +259 -16
- package/src/runtime/routes/log-export-routes.ts +42 -22
- package/src/runtime/routes/memory-item-routes.ts +1 -7
- package/src/runtime/routes/migration-routes.ts +87 -2
- package/src/runtime/routes/oauth-apps.ts +15 -17
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/schedule-routes.ts +24 -11
- package/src/runtime/routes/settings-routes.ts +9 -97
- package/src/runtime/routes/skills-routes.ts +52 -2
- package/src/runtime/routes/subagents-routes.ts +14 -10
- package/src/runtime/routes/usage-routes.ts +8 -7
- package/src/runtime/routes/workspace-routes.test.ts +22 -0
- package/src/runtime/routes/workspace-routes.ts +8 -1
- package/src/runtime/routes/workspace-utils.ts +2 -0
- package/src/schedule/scheduler.ts +7 -5
- package/src/security/ces-credential-client.ts +20 -0
- package/src/security/ces-rpc-credential-backend.ts +17 -0
- package/src/security/credential-backend.ts +5 -0
- package/src/security/oauth2.ts +42 -25
- package/src/security/secure-keys.ts +118 -25
- package/src/security/token-manager.ts +23 -10
- package/src/skills/catalog-files.ts +492 -0
- package/src/subagent/manager.ts +131 -26
- package/src/subagent/types.ts +19 -0
- package/src/tools/apps/executors.ts +11 -2
- package/src/tools/browser/__tests__/auth-detector.test.ts +202 -108
- package/src/tools/browser/auth-detector.ts +43 -12
- package/src/tools/browser/browser-execution.ts +645 -340
- package/src/tools/browser/browser-manager.ts +36 -12
- package/src/tools/browser/cdp-client/__tests__/accessibility-snapshot.test.ts +318 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-dom-helpers.test.ts +1175 -0
- package/src/tools/browser/cdp-client/__tests__/cdp-inspect-client.test.ts +870 -0
- package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +330 -0
- package/src/tools/browser/cdp-client/__tests__/factory.test.ts +377 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-nested-frames.json +64 -0
- package/src/tools/browser/cdp-client/__tests__/fixtures/ax-tree-simple.json +69 -0
- package/src/tools/browser/cdp-client/__tests__/local-cdp-client.test.ts +310 -0
- package/src/tools/browser/cdp-client/__tests__/types.test.ts +96 -0
- package/src/tools/browser/cdp-client/accessibility-snapshot.ts +387 -0
- package/src/tools/browser/cdp-client/cdp-dom-helpers.ts +695 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/discovery.test.ts +743 -0
- package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +580 -0
- package/src/tools/browser/cdp-client/cdp-inspect/discovery.ts +578 -0
- package/src/tools/browser/cdp-client/cdp-inspect/ws-transport.ts +579 -0
- package/src/tools/browser/cdp-client/cdp-inspect-client.ts +635 -0
- package/src/tools/browser/cdp-client/errors.ts +34 -0
- package/src/tools/browser/cdp-client/extension-cdp-client.ts +125 -0
- package/src/tools/browser/cdp-client/factory.ts +204 -0
- package/src/tools/browser/cdp-client/index.ts +14 -0
- package/src/tools/browser/cdp-client/local-cdp-client.ts +187 -0
- package/src/tools/browser/cdp-client/types.ts +52 -0
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +2 -1
- package/src/tools/host-filesystem/edit.ts +1 -1
- package/src/tools/host-filesystem/read.ts +12 -15
- package/src/tools/host-filesystem/write.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +21 -16
- package/src/tools/permission-checker.ts +77 -82
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -0
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- package/src/tools/subagent/spawn.ts +47 -3
- package/src/tools/subagent/status.ts +2 -0
- package/src/tools/system/register.ts +2 -16
- package/src/tools/terminal/safe-env.ts +7 -0
- package/src/tools/terminal/shell.ts +21 -16
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/types.ts +2 -0
- package/src/util/platform.ts +14 -19
- package/src/workspace/top-level-renderer.ts +19 -1
- package/src/__tests__/chrome-cdp.test.ts +0 -419
- package/src/__tests__/permission-mode-sse.test.ts +0 -418
- package/src/__tests__/permission-mode-store.test.ts +0 -277
- package/src/browser-extension-relay/protocol.ts +0 -63
- package/src/browser-extension-relay/server.ts +0 -203
- package/src/config/schemas/sandbox.ts +0 -14
- package/src/permissions/permission-mode-store.ts +0 -180
- package/src/tools/browser/chrome-cdp.ts +0 -239
- package/src/tools/system/set-permission-mode.ts +0 -103
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route handler for `POST /v1/browser-extension-pair`.
|
|
3
|
+
*
|
|
4
|
+
* Mints a short-lived, scoped `host_browser_command` capability token for a
|
|
5
|
+
* chrome extension that has proved (via the native messaging helper) it is
|
|
6
|
+
* running locally with an allowlisted extension id.
|
|
7
|
+
*
|
|
8
|
+
* Security properties:
|
|
9
|
+
* - **Localhost-only**: enforced by both the TCP peer IP (via
|
|
10
|
+
* `server.requestIP`) and the `Host` header. Non-localhost callers
|
|
11
|
+
* receive a 403.
|
|
12
|
+
* - **Native-host marker header**: the request must carry the
|
|
13
|
+
* `x-vellum-native-host: 1` marker. Only the native messaging helper
|
|
14
|
+
* sets this header; browsers cannot attach custom request headers to
|
|
15
|
+
* fetches from web pages (custom headers trip CORS preflight, which
|
|
16
|
+
* this endpoint does not accept). Missing marker header is rejected
|
|
17
|
+
* with 403.
|
|
18
|
+
* - **Browser-origin rejection**: if an `Origin` header is present it
|
|
19
|
+
* must be either empty or explicitly on the
|
|
20
|
+
* `ALLOWED_EXTENSION_ORIGINS` allowlist. This defends against a
|
|
21
|
+
* malicious web page in another tab issuing a cross-origin POST from
|
|
22
|
+
* the user's browser — such a request would carry the page's origin
|
|
23
|
+
* and would be rejected here even if it somehow reached loopback.
|
|
24
|
+
* - **Strict rate limiting**: a dedicated per-peer sliding-window
|
|
25
|
+
* limiter caps pair requests at 10/minute per peer IP. This is
|
|
26
|
+
* separate from the global API limiter because the pair endpoint
|
|
27
|
+
* is pre-auth and extra abuse-sensitive.
|
|
28
|
+
* - **Audit logs on denial**: every rejected request emits a structured
|
|
29
|
+
* warn log including peer IP, Host header, Origin header, native-host
|
|
30
|
+
* marker presence, and a reason code so operators can triage denied
|
|
31
|
+
* attempts.
|
|
32
|
+
* - **Origin allowlist**: the body must include `extensionOrigin`
|
|
33
|
+
* matching a hard-coded allowlist of known Vellum chrome extension
|
|
34
|
+
* ids. This is treated as a secondary defense — the primary gate is
|
|
35
|
+
* the native-host marker header plus localhost peer check.
|
|
36
|
+
*
|
|
37
|
+
* Request body: `{ extensionOrigin: string }` (also accepts the legacy
|
|
38
|
+
* `{ origin: string }` for backwards compatibility).
|
|
39
|
+
* Response body: `{ token, expiresAt, guardianId }` — `expiresAt` is an
|
|
40
|
+
* ISO 8601 timestamp string matching what the native
|
|
41
|
+
* messaging helper validates.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { readFileSync } from "node:fs";
|
|
45
|
+
import { resolve } from "node:path";
|
|
46
|
+
|
|
47
|
+
import { findGuardianForChannel } from "../../contacts/contact-store.js";
|
|
48
|
+
import { getLogger } from "../../util/logger.js";
|
|
49
|
+
import { mintHostBrowserCapability } from "../capability-tokens.js";
|
|
50
|
+
import { httpError } from "../http-errors.js";
|
|
51
|
+
import { isLoopbackAddress } from "../middleware/auth.js";
|
|
52
|
+
import { TokenRateLimiter } from "../middleware/rate-limiter.js";
|
|
53
|
+
|
|
54
|
+
const log = getLogger("browser-extension-pair");
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Header name the native messaging helper MUST set on pair requests.
|
|
58
|
+
* Exported for tests and for the helper to keep in sync. Browsers cannot
|
|
59
|
+
* attach custom headers to fetches from web pages without tripping CORS
|
|
60
|
+
* preflight, which this endpoint does not handle — so a request carrying
|
|
61
|
+
* this header is (by construction) not a drive-by browser call.
|
|
62
|
+
*/
|
|
63
|
+
export const NATIVE_HOST_MARKER_HEADER = "x-vellum-native-host";
|
|
64
|
+
|
|
65
|
+
/** Expected value for the native-host marker header. */
|
|
66
|
+
export const NATIVE_HOST_MARKER_VALUE = "1";
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Strict per-peer rate limit for pair requests: 10 requests/minute per
|
|
70
|
+
* loopback peer IP. The native messaging flow only issues one pair
|
|
71
|
+
* request per extension spawn, so this budget is generous for normal
|
|
72
|
+
* use (account for retries on transient failures) while still clamping
|
|
73
|
+
* any abuse surface if a local attacker somehow invokes the endpoint
|
|
74
|
+
* in a tight loop. Exported for tests that need to reset state.
|
|
75
|
+
*/
|
|
76
|
+
const PAIR_RATE_LIMIT_MAX_REQUESTS = 10;
|
|
77
|
+
const PAIR_RATE_LIMIT_WINDOW_MS = 60_000;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Dedicated rate limiter instance for the pair endpoint. Keyed on the
|
|
81
|
+
* TCP peer IP (always loopback here, so the key space is tiny and a
|
|
82
|
+
* handful of tracked keys is plenty).
|
|
83
|
+
*/
|
|
84
|
+
const pairRateLimiter = new TokenRateLimiter(
|
|
85
|
+
PAIR_RATE_LIMIT_MAX_REQUESTS,
|
|
86
|
+
PAIR_RATE_LIMIT_WINDOW_MS,
|
|
87
|
+
64,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/** Bun server shape needed for requestIP. */
|
|
91
|
+
export type PairServerContext = {
|
|
92
|
+
requestIP(
|
|
93
|
+
req: Request,
|
|
94
|
+
): { address: string; family: string; port: number } | null;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const EXTENSION_ID_REGEX = /^[a-p]{32}$/;
|
|
98
|
+
const ALLOWLIST_CONFIG_PATH_CANDIDATES = [
|
|
99
|
+
// Source-checkout / test path (works when running from repo).
|
|
100
|
+
resolve(
|
|
101
|
+
import.meta.dir,
|
|
102
|
+
"..",
|
|
103
|
+
"..",
|
|
104
|
+
"..",
|
|
105
|
+
"..",
|
|
106
|
+
"meta",
|
|
107
|
+
"browser-extension",
|
|
108
|
+
"chrome-extension-allowlist.json",
|
|
109
|
+
),
|
|
110
|
+
// Repo-root current-working-directory fallback.
|
|
111
|
+
resolve(
|
|
112
|
+
process.cwd(),
|
|
113
|
+
"meta",
|
|
114
|
+
"browser-extension",
|
|
115
|
+
"chrome-extension-allowlist.json",
|
|
116
|
+
),
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
type ChromeExtensionAllowlistConfig = {
|
|
120
|
+
version: number;
|
|
121
|
+
allowedExtensionIds: string[];
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
function parseAllowedExtensionIds(value: unknown): string[] {
|
|
125
|
+
if (!Array.isArray(value)) {
|
|
126
|
+
throw new Error("allowedExtensionIds is not an array");
|
|
127
|
+
}
|
|
128
|
+
const ids = value
|
|
129
|
+
.filter((id): id is string => typeof id === "string")
|
|
130
|
+
.filter((id) => EXTENSION_ID_REGEX.test(id));
|
|
131
|
+
if (ids.length === 0) {
|
|
132
|
+
throw new Error("allowedExtensionIds has no valid extension ids");
|
|
133
|
+
}
|
|
134
|
+
return ids;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function loadAllowedExtensionIdsFromEnv(): string[] {
|
|
138
|
+
const raw =
|
|
139
|
+
process.env.VELLUM_CHROME_EXTENSION_IDS ??
|
|
140
|
+
process.env.VELLUM_CHROME_EXTENSION_ID;
|
|
141
|
+
if (!raw) return [];
|
|
142
|
+
const ids = raw
|
|
143
|
+
.split(/[,\s]+/)
|
|
144
|
+
.map((id) => id.trim())
|
|
145
|
+
.filter((id) => id.length > 0)
|
|
146
|
+
.filter((id) => EXTENSION_ID_REGEX.test(id));
|
|
147
|
+
return Array.from(new Set(ids));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function loadAllowedExtensionOrigins(): ReadonlySet<string> {
|
|
151
|
+
const loadErrors: string[] = [];
|
|
152
|
+
for (const configPath of ALLOWLIST_CONFIG_PATH_CANDIDATES) {
|
|
153
|
+
try {
|
|
154
|
+
const raw = readFileSync(configPath, "utf8");
|
|
155
|
+
const parsed = JSON.parse(raw) as Partial<ChromeExtensionAllowlistConfig>;
|
|
156
|
+
const ids = parseAllowedExtensionIds(parsed.allowedExtensionIds);
|
|
157
|
+
return new Set<string>(ids.map((id) => `chrome-extension://${id}/`));
|
|
158
|
+
} catch (err) {
|
|
159
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
160
|
+
loadErrors.push(`${configPath}: ${detail}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Compiled Bun binaries run from a virtual FS root (import.meta.dir is
|
|
165
|
+
// usually `/$bunfs/root`), so repo-relative config paths can disappear in
|
|
166
|
+
// packaged builds. In that case, allow a build-time injected env fallback.
|
|
167
|
+
const envIds = loadAllowedExtensionIdsFromEnv();
|
|
168
|
+
if (envIds.length > 0) {
|
|
169
|
+
return new Set<string>(envIds.map((id) => `chrome-extension://${id}/`));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
log.error(
|
|
173
|
+
{
|
|
174
|
+
allowlistConfigPathCandidates: ALLOWLIST_CONFIG_PATH_CANDIDATES,
|
|
175
|
+
loadErrors,
|
|
176
|
+
},
|
|
177
|
+
"Failed to load Chrome extension allowlist config; pairing will reject all origins",
|
|
178
|
+
);
|
|
179
|
+
return new Set<string>();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Allowlist of chrome extension origins permitted to request a capability
|
|
184
|
+
* token. Loaded from the canonical config at
|
|
185
|
+
* `meta/browser-extension/chrome-extension-allowlist.json`.
|
|
186
|
+
*/
|
|
187
|
+
export const ALLOWED_EXTENSION_ORIGINS = loadAllowedExtensionOrigins();
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Reset the dedicated pair-endpoint rate limiter. Exported for tests
|
|
191
|
+
* so one test's burst can't bleed into another. Production code never
|
|
192
|
+
* calls this.
|
|
193
|
+
*
|
|
194
|
+
* We reach into the private `requests` map via a typed cast rather
|
|
195
|
+
* than adding a `reset()` method to the shared `TokenRateLimiter` —
|
|
196
|
+
* the limiter is a general-purpose utility that other routes also
|
|
197
|
+
* use, and we don't want to pollute its public API with a test-only
|
|
198
|
+
* escape hatch.
|
|
199
|
+
*/
|
|
200
|
+
export function resetPairRateLimiterForTests(): void {
|
|
201
|
+
const limiter = pairRateLimiter as unknown as {
|
|
202
|
+
requests: Map<string, unknown>;
|
|
203
|
+
};
|
|
204
|
+
limiter.requests.clear();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Parse an HTTP `Host` header value and extract the hostname portion.
|
|
209
|
+
*
|
|
210
|
+
* Handles IPv6 bracket notation (`[::1]:8765`), unbracketed IPv6
|
|
211
|
+
* (`::1`), hostname with port (`localhost:8765`), and bare hostnames
|
|
212
|
+
* (`localhost`). Returns `null` when the header is malformed (e.g.
|
|
213
|
+
* missing closing bracket, or content after the closing bracket that
|
|
214
|
+
* isn't an optional `:port`).
|
|
215
|
+
*
|
|
216
|
+
* Exported for testing.
|
|
217
|
+
*/
|
|
218
|
+
export function parseHostHeader(raw: string): string | null {
|
|
219
|
+
if (raw.length === 0) return null;
|
|
220
|
+
// IPv6 literal in brackets, e.g. `[::1]` or `[::1]:8765`.
|
|
221
|
+
if (raw.startsWith("[")) {
|
|
222
|
+
const end = raw.indexOf("]");
|
|
223
|
+
if (end < 0) return null;
|
|
224
|
+
// After the closing bracket only an optional ":port" is valid. Anything
|
|
225
|
+
// else (e.g. `[::1]attacker.com`) is a malformed Host header that an
|
|
226
|
+
// attacker could craft to slip a non-loopback hostname past the parser.
|
|
227
|
+
const after = raw.substring(end + 1);
|
|
228
|
+
if (after.length > 0 && !after.startsWith(":")) return null;
|
|
229
|
+
return raw.substring(1, end);
|
|
230
|
+
}
|
|
231
|
+
// Bare IPv6 (no brackets) contains multiple colons and should be
|
|
232
|
+
// treated as a whole. Anything with a single colon is `host:port`.
|
|
233
|
+
const firstColon = raw.indexOf(":");
|
|
234
|
+
if (firstColon < 0) return raw;
|
|
235
|
+
const secondColon = raw.indexOf(":", firstColon + 1);
|
|
236
|
+
if (secondColon >= 0) {
|
|
237
|
+
// Multiple colons and no brackets — assume unbracketed IPv6.
|
|
238
|
+
return raw;
|
|
239
|
+
}
|
|
240
|
+
return raw.substring(0, firstColon);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Returns true if the Host header (if present) points at a loopback
|
|
245
|
+
* address. We accept a missing Host header because some HTTP clients
|
|
246
|
+
* (notably node test harnesses) omit it.
|
|
247
|
+
*/
|
|
248
|
+
function isLoopbackHostHeader(host: string | null): boolean {
|
|
249
|
+
if (!host) return true;
|
|
250
|
+
const parsed = parseHostHeader(host);
|
|
251
|
+
if (parsed === null) return false;
|
|
252
|
+
const hostname = parsed.toLowerCase();
|
|
253
|
+
if (hostname === "localhost") return true;
|
|
254
|
+
if (hostname === "127.0.0.1") return true;
|
|
255
|
+
if (hostname === "::1") return true;
|
|
256
|
+
if (hostname.startsWith("127.")) {
|
|
257
|
+
// Matches the 127.0.0.0/8 loopback range (e.g. 127.0.0.1, 127.1.2.3).
|
|
258
|
+
const parts = hostname.split(".");
|
|
259
|
+
if (parts.length !== 4) return false;
|
|
260
|
+
return parts.every((p) => /^\d+$/.test(p) && Number(p) <= 255);
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Resolve the guardian id to bind the capability token to. Phase 2 uses
|
|
267
|
+
* the local vellum guardian principal when one exists, falling back to
|
|
268
|
+
* the string `"local"` for fresh installs that haven't bootstrapped a
|
|
269
|
+
* guardian yet.
|
|
270
|
+
*/
|
|
271
|
+
function resolveLocalGuardianId(): string {
|
|
272
|
+
try {
|
|
273
|
+
const result = findGuardianForChannel("vellum");
|
|
274
|
+
if (result?.contact.principalId) {
|
|
275
|
+
return result.contact.principalId;
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
log.warn(
|
|
279
|
+
{ err },
|
|
280
|
+
"Failed to look up local vellum guardian; falling back to 'local'",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return "local";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Emit an audit log for a denied pair attempt. Centralizes the field
|
|
288
|
+
* shape (peer IP, host header, origin header, native-host marker
|
|
289
|
+
* presence, reason) so operators can grep for a single log signature
|
|
290
|
+
* when triaging abuse.
|
|
291
|
+
*/
|
|
292
|
+
function auditDeny(
|
|
293
|
+
req: Request,
|
|
294
|
+
peerIp: string,
|
|
295
|
+
reason: string,
|
|
296
|
+
extra?: Record<string, unknown>,
|
|
297
|
+
): void {
|
|
298
|
+
const host = req.headers.get("host");
|
|
299
|
+
const origin = req.headers.get("origin");
|
|
300
|
+
const nativeHostMarker = req.headers.get(NATIVE_HOST_MARKER_HEADER);
|
|
301
|
+
log.warn(
|
|
302
|
+
{
|
|
303
|
+
audit: "browser-extension-pair-denied",
|
|
304
|
+
peerIp,
|
|
305
|
+
host,
|
|
306
|
+
origin,
|
|
307
|
+
nativeHostMarkerPresent: nativeHostMarker !== null,
|
|
308
|
+
reason,
|
|
309
|
+
...extra,
|
|
310
|
+
},
|
|
311
|
+
`pair_denied: ${reason}`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Handle POST /v1/browser-extension-pair.
|
|
317
|
+
*
|
|
318
|
+
* Body: `{ extensionOrigin: string }` (also accepts legacy
|
|
319
|
+
* `{ origin: string }` for backwards compatibility).
|
|
320
|
+
* Returns: `{ token, expiresAt, guardianId }` where `expiresAt` is an
|
|
321
|
+
* ISO 8601 timestamp string that the native messaging helper
|
|
322
|
+
* validates as a string.
|
|
323
|
+
*/
|
|
324
|
+
export async function handleBrowserExtensionPair(
|
|
325
|
+
req: Request,
|
|
326
|
+
server: PairServerContext,
|
|
327
|
+
): Promise<Response> {
|
|
328
|
+
if (req.method !== "POST") {
|
|
329
|
+
return new Response("method not allowed", {
|
|
330
|
+
status: 405,
|
|
331
|
+
headers: { Allow: "POST" },
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Enforce localhost-only via peer IP.
|
|
336
|
+
const peer = server.requestIP(req);
|
|
337
|
+
const peerIp = peer?.address ?? "";
|
|
338
|
+
if (!peerIp || !isLoopbackAddress(peerIp)) {
|
|
339
|
+
auditDeny(req, peerIp, "non_loopback_peer");
|
|
340
|
+
return httpError("FORBIDDEN", "endpoint is local-only", 403);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Secondary check: Host header. Rejects requests that slip past the
|
|
344
|
+
// TCP-level check via proxies that rewrite the peer address.
|
|
345
|
+
const host = req.headers.get("host");
|
|
346
|
+
if (!isLoopbackHostHeader(host)) {
|
|
347
|
+
auditDeny(req, peerIp, "non_loopback_host_header");
|
|
348
|
+
return httpError("FORBIDDEN", "endpoint is local-only", 403);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Any `x-forwarded-for` header indicates the request was proxied from a
|
|
352
|
+
// non-local client. Reject — the pair endpoint is strictly machine-local.
|
|
353
|
+
if (req.headers.get("x-forwarded-for")) {
|
|
354
|
+
auditDeny(req, peerIp, "x_forwarded_for_present");
|
|
355
|
+
return httpError("FORBIDDEN", "endpoint is local-only", 403);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Primary marker-header gate. The native messaging helper sets this
|
|
359
|
+
// header on every pair request; browsers cannot (without CORS
|
|
360
|
+
// preflight, which this endpoint does not serve). Reject when the
|
|
361
|
+
// header is absent or set to an unexpected value.
|
|
362
|
+
//
|
|
363
|
+
// IMPORTANT: this check runs BEFORE the rate limiter so that
|
|
364
|
+
// unmarked drive-by POSTs from a malicious webpage cannot burn the
|
|
365
|
+
// legitimate 10/min budget. If the rate limiter ran first, a
|
|
366
|
+
// cross-origin page could issue 10 unmarked requests per minute and
|
|
367
|
+
// starve the native messaging helper's real pair attempts with 429s
|
|
368
|
+
// until the window reset. Unmarked requests therefore return 403
|
|
369
|
+
// without touching the limiter at all.
|
|
370
|
+
const marker = req.headers.get(NATIVE_HOST_MARKER_HEADER);
|
|
371
|
+
if (marker !== NATIVE_HOST_MARKER_VALUE) {
|
|
372
|
+
auditDeny(req, peerIp, "missing_native_host_marker");
|
|
373
|
+
return httpError("FORBIDDEN", "native host marker required", 403);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Strict rate limit by peer IP. The limiter is keyed on the loopback
|
|
377
|
+
// peer address; browsers (even local ones) all appear as 127.0.0.1
|
|
378
|
+
// here, which is intentional — a single compromised local process
|
|
379
|
+
// should not be able to hammer the mint endpoint. We evaluate this
|
|
380
|
+
// AFTER the native-host marker check so that unauthenticated
|
|
381
|
+
// drive-by POSTs can't consume the legitimate 10/min quota (see
|
|
382
|
+
// comment above the marker check for the DoS rationale).
|
|
383
|
+
const rateResult = pairRateLimiter.check(
|
|
384
|
+
peerIp,
|
|
385
|
+
"/v1/browser-extension-pair",
|
|
386
|
+
);
|
|
387
|
+
if (!rateResult.allowed) {
|
|
388
|
+
auditDeny(req, peerIp, "rate_limited", {
|
|
389
|
+
limit: rateResult.limit,
|
|
390
|
+
resetAt: rateResult.resetAt,
|
|
391
|
+
});
|
|
392
|
+
const retryAfter = Math.max(
|
|
393
|
+
1,
|
|
394
|
+
rateResult.resetAt - Math.ceil(Date.now() / 1000),
|
|
395
|
+
);
|
|
396
|
+
// Return the same error envelope shape as `httpError` but with
|
|
397
|
+
// Retry-After + X-RateLimit-* headers attached so the native
|
|
398
|
+
// host can back off sensibly. We construct the body inline to
|
|
399
|
+
// avoid cloning / re-consuming a Response returned by
|
|
400
|
+
// `httpError` (Response bodies are one-shot streams).
|
|
401
|
+
return Response.json(
|
|
402
|
+
{
|
|
403
|
+
error: {
|
|
404
|
+
code: "RATE_LIMITED",
|
|
405
|
+
message: "too many pair requests",
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
status: 429,
|
|
410
|
+
headers: {
|
|
411
|
+
"Retry-After": String(retryAfter),
|
|
412
|
+
"X-RateLimit-Limit": String(rateResult.limit),
|
|
413
|
+
"X-RateLimit-Remaining": "0",
|
|
414
|
+
"X-RateLimit-Reset": String(rateResult.resetAt),
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Browser-origin rejection. Any non-empty `Origin` header that isn't
|
|
421
|
+
// on the extension origin allowlist is a cross-origin browser fetch
|
|
422
|
+
// and must be rejected. The native messaging helper sends no Origin
|
|
423
|
+
// header at all (it's a plain node fetch, not a browser fetch), so
|
|
424
|
+
// the common case is `origin === null`.
|
|
425
|
+
const originHeader = req.headers.get("origin");
|
|
426
|
+
if (originHeader !== null && originHeader.length > 0) {
|
|
427
|
+
// Normalize by stripping any trailing slash mismatch: the
|
|
428
|
+
// allowlist entries end with `/` but browsers' Origin headers
|
|
429
|
+
// never include a trailing slash (per RFC 6454 an origin is
|
|
430
|
+
// scheme+host+port with no path). Compare both the bare origin
|
|
431
|
+
// and the `/`-suffixed form against the allowlist.
|
|
432
|
+
const withSlash = `${originHeader}/`;
|
|
433
|
+
if (
|
|
434
|
+
!ALLOWED_EXTENSION_ORIGINS.has(originHeader) &&
|
|
435
|
+
!ALLOWED_EXTENSION_ORIGINS.has(withSlash)
|
|
436
|
+
) {
|
|
437
|
+
auditDeny(req, peerIp, "browser_origin_not_allowlisted", {
|
|
438
|
+
originHeader,
|
|
439
|
+
});
|
|
440
|
+
return httpError("FORBIDDEN", "origin not allowed", 403);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let body: unknown;
|
|
445
|
+
try {
|
|
446
|
+
body = await req.json();
|
|
447
|
+
} catch {
|
|
448
|
+
auditDeny(req, peerIp, "invalid_json_body");
|
|
449
|
+
return httpError("BAD_REQUEST", "invalid JSON body", 400);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (!body || typeof body !== "object") {
|
|
453
|
+
auditDeny(req, peerIp, "body_not_object");
|
|
454
|
+
return httpError("BAD_REQUEST", "body must be an object", 400);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Accept `extensionOrigin` (preferred, matches the native messaging
|
|
458
|
+
// helper) and fall back to `origin` (legacy, for any callers that
|
|
459
|
+
// haven't migrated yet).
|
|
460
|
+
const raw = body as {
|
|
461
|
+
extensionOrigin?: unknown;
|
|
462
|
+
origin?: unknown;
|
|
463
|
+
};
|
|
464
|
+
const extensionOrigin =
|
|
465
|
+
typeof raw.extensionOrigin === "string" && raw.extensionOrigin.length > 0
|
|
466
|
+
? raw.extensionOrigin
|
|
467
|
+
: typeof raw.origin === "string" && raw.origin.length > 0
|
|
468
|
+
? raw.origin
|
|
469
|
+
: null;
|
|
470
|
+
if (extensionOrigin === null) {
|
|
471
|
+
auditDeny(req, peerIp, "missing_extension_origin");
|
|
472
|
+
return httpError("BAD_REQUEST", "extensionOrigin is required", 400);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Secondary defense: body-level extension origin allowlist. The
|
|
476
|
+
// primary gate is the native-host marker + loopback peer check; this
|
|
477
|
+
// check catches the failure mode where a compromised extension id
|
|
478
|
+
// that doesn't match a known Vellum build still manages to reach
|
|
479
|
+
// the endpoint.
|
|
480
|
+
if (!ALLOWED_EXTENSION_ORIGINS.has(extensionOrigin)) {
|
|
481
|
+
auditDeny(req, peerIp, "extension_origin_not_allowlisted", {
|
|
482
|
+
extensionOrigin,
|
|
483
|
+
});
|
|
484
|
+
return httpError("UNAUTHORIZED", "unauthorized origin", 401);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const guardianId = resolveLocalGuardianId();
|
|
488
|
+
const { token, expiresAt } = mintHostBrowserCapability(guardianId);
|
|
489
|
+
const expiresAtIso = new Date(expiresAt).toISOString();
|
|
490
|
+
|
|
491
|
+
log.info(
|
|
492
|
+
{ extensionOrigin, guardianId, expiresAt: expiresAtIso },
|
|
493
|
+
"Issued chrome extension capability token",
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
return Response.json({ token, expiresAt: expiresAtIso, guardianId });
|
|
497
|
+
}
|
|
@@ -111,6 +111,7 @@ Analyze the conversation above. Provide a structured self-assessment:
|
|
|
111
111
|
3. **What went wrong**: Errors, unnecessary tool calls, incorrect assumptions, wasted turns, misunderstandings.
|
|
112
112
|
4. **Root causes**: Why did failures happen? Missing context? Wrong approach? Tool limitations?
|
|
113
113
|
5. **Recommendations**: Specific, actionable improvements for similar conversations next time.
|
|
114
|
+
6. **Code & tooling changes**: Are there any changes to files you should make based on these learnings? Are there any skills or scripts that are worth creating or modifying? Don't make these changes yet — just provide your analysis.
|
|
114
115
|
|
|
115
116
|
Be honest and specific. Reference particular moments in the transcript. Focus on patterns that generalize beyond this specific conversation.
|
|
116
117
|
|
|
@@ -159,7 +160,7 @@ Do not use tools during analysis. If you identify insights worth remembering for
|
|
|
159
160
|
// l. Fire-and-forget the agent loop
|
|
160
161
|
analysisConversation
|
|
161
162
|
.runAgentLoop(prompt, messageId, onEvent, {
|
|
162
|
-
isInteractive:
|
|
163
|
+
isInteractive: false,
|
|
163
164
|
isUserMessage: true,
|
|
164
165
|
})
|
|
165
166
|
.catch((err) => {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* POST /v1/conversations — create a new conversation
|
|
5
5
|
* POST /v1/conversations/switch — switch to an existing conversation
|
|
6
6
|
* POST /v1/conversations/fork — fork an existing conversation
|
|
7
|
+
* GET /v1/conversations/:id/host-access — read host access for one conversation
|
|
8
|
+
* PATCH /v1/conversations/:id/host-access — update host access for one conversation
|
|
7
9
|
* PATCH /v1/conversations/:id/name — rename a conversation
|
|
8
10
|
* DELETE /v1/conversations — clear all conversations
|
|
9
11
|
* POST /v1/conversations/:id/wipe — wipe conversation and revert memory
|
|
@@ -21,7 +23,9 @@ import {
|
|
|
21
23
|
countConversationsByScheduleJobId,
|
|
22
24
|
deleteConversation,
|
|
23
25
|
getConversation,
|
|
26
|
+
getConversationHostAccess,
|
|
24
27
|
PRIVATE_CONVERSATION_FORK_ERROR,
|
|
28
|
+
updateConversationHostAccess,
|
|
25
29
|
wipeConversation,
|
|
26
30
|
} from "../../memory/conversation-crud.js";
|
|
27
31
|
import { updateConversationTitle } from "../../memory/conversation-crud.js";
|
|
@@ -34,6 +38,10 @@ import { enqueueMemoryJob } from "../../memory/jobs-store.js";
|
|
|
34
38
|
import { deleteSchedule } from "../../schedule/schedule-store.js";
|
|
35
39
|
import { UserError } from "../../util/errors.js";
|
|
36
40
|
import { getLogger } from "../../util/logger.js";
|
|
41
|
+
import { buildAssistantEvent } from "../assistant-event.js";
|
|
42
|
+
import { assistantEventHub } from "../assistant-event-hub.js";
|
|
43
|
+
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
44
|
+
import { requireBoundGuardian } from "../auth/require-bound-guardian.js";
|
|
37
45
|
import { httpError } from "../http-errors.js";
|
|
38
46
|
import type { RouteDefinition } from "../http-router.js";
|
|
39
47
|
|
|
@@ -52,6 +60,7 @@ export interface ConversationManagementDeps {
|
|
|
52
60
|
conversationId: string;
|
|
53
61
|
title: string;
|
|
54
62
|
conversationType: string;
|
|
63
|
+
hostAccess: boolean;
|
|
55
64
|
} | null>;
|
|
56
65
|
renameConversation: (conversationId: string, name: string) => boolean;
|
|
57
66
|
clearAllConversations: () => number;
|
|
@@ -218,6 +227,7 @@ export function conversationManagementRouteDefinitions(
|
|
|
218
227
|
conversationId: z.string(),
|
|
219
228
|
title: z.string(),
|
|
220
229
|
conversationType: z.string(),
|
|
230
|
+
hostAccess: z.boolean(),
|
|
221
231
|
}),
|
|
222
232
|
handler: async ({ req }) => {
|
|
223
233
|
const body = (await req.json()) as {
|
|
@@ -246,6 +256,104 @@ export function conversationManagementRouteDefinitions(
|
|
|
246
256
|
title: result.title,
|
|
247
257
|
conversationType:
|
|
248
258
|
result.conversationType === "private" ? "private" : "standard",
|
|
259
|
+
hostAccess: result.hostAccess,
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
endpoint: "conversations/:id/host-access",
|
|
265
|
+
method: "GET",
|
|
266
|
+
policyKey: "conversations/host-access:GET",
|
|
267
|
+
summary: "Get conversation host access",
|
|
268
|
+
description: "Return whether the conversation can use host tools.",
|
|
269
|
+
tags: ["conversations"],
|
|
270
|
+
responseBody: z.object({
|
|
271
|
+
conversationId: z.string(),
|
|
272
|
+
hostAccess: z.boolean(),
|
|
273
|
+
}),
|
|
274
|
+
handler: ({ params }) => {
|
|
275
|
+
const resolvedId = resolveConversationId(params.id) ?? params.id;
|
|
276
|
+
const conversation = getConversation(resolvedId);
|
|
277
|
+
if (!conversation) {
|
|
278
|
+
return httpError(
|
|
279
|
+
"NOT_FOUND",
|
|
280
|
+
`Conversation ${params.id} not found`,
|
|
281
|
+
404,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return Response.json({
|
|
285
|
+
conversationId: conversation.id,
|
|
286
|
+
hostAccess: getConversationHostAccess(conversation.id),
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
endpoint: "conversations/:id/host-access",
|
|
292
|
+
method: "PATCH",
|
|
293
|
+
policyKey: "conversations/host-access",
|
|
294
|
+
summary: "Update conversation host access",
|
|
295
|
+
description: "Enable or disable host access for a conversation.",
|
|
296
|
+
tags: ["conversations"],
|
|
297
|
+
requestBody: z.object({
|
|
298
|
+
hostAccess: z.boolean(),
|
|
299
|
+
}),
|
|
300
|
+
responseBody: z.object({
|
|
301
|
+
conversationId: z.string(),
|
|
302
|
+
hostAccess: z.boolean(),
|
|
303
|
+
}),
|
|
304
|
+
handler: async ({ req, params, authContext }) => {
|
|
305
|
+
const guardianError = requireBoundGuardian(authContext);
|
|
306
|
+
if (guardianError) return guardianError;
|
|
307
|
+
|
|
308
|
+
const rawBody = (await req.json()) as unknown;
|
|
309
|
+
if (
|
|
310
|
+
rawBody == null ||
|
|
311
|
+
typeof rawBody !== "object" ||
|
|
312
|
+
Array.isArray(rawBody)
|
|
313
|
+
) {
|
|
314
|
+
return httpError("BAD_REQUEST", "Invalid request body", 400);
|
|
315
|
+
}
|
|
316
|
+
const body = rawBody as { hostAccess?: unknown };
|
|
317
|
+
if (typeof body.hostAccess !== "boolean") {
|
|
318
|
+
return httpError("BAD_REQUEST", "Missing hostAccess boolean", 400);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const resolvedId = resolveConversationId(params.id) ?? params.id;
|
|
322
|
+
const conversation = getConversation(resolvedId);
|
|
323
|
+
if (!conversation) {
|
|
324
|
+
return httpError(
|
|
325
|
+
"NOT_FOUND",
|
|
326
|
+
`Conversation ${params.id} not found`,
|
|
327
|
+
404,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const nextHostAccess = body.hostAccess;
|
|
332
|
+
if (conversation.hostAccess !== (nextHostAccess ? 1 : 0)) {
|
|
333
|
+
updateConversationHostAccess(resolvedId, nextHostAccess);
|
|
334
|
+
assistantEventHub
|
|
335
|
+
.publish(
|
|
336
|
+
buildAssistantEvent(
|
|
337
|
+
DAEMON_INTERNAL_ASSISTANT_ID,
|
|
338
|
+
{
|
|
339
|
+
type: "conversation_host_access_updated",
|
|
340
|
+
conversationId: resolvedId,
|
|
341
|
+
hostAccess: nextHostAccess,
|
|
342
|
+
},
|
|
343
|
+
resolvedId,
|
|
344
|
+
),
|
|
345
|
+
)
|
|
346
|
+
.catch((err) => {
|
|
347
|
+
log.warn(
|
|
348
|
+
{ err, conversationId: resolvedId },
|
|
349
|
+
"Failed to publish conversation_host_access_updated event",
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return Response.json({
|
|
355
|
+
conversationId: resolvedId,
|
|
356
|
+
hostAccess: nextHostAccess,
|
|
249
357
|
});
|
|
250
358
|
},
|
|
251
359
|
},
|