@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
|
@@ -5,13 +5,18 @@
|
|
|
5
5
|
* configured port (default: 7821).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { dirname, resolve } from "node:path";
|
|
10
17
|
|
|
11
18
|
import type { ServerWebSocket } from "bun";
|
|
12
19
|
|
|
13
|
-
import type { BrowserRelayWebSocketData } from "../browser-extension-relay/server.js";
|
|
14
|
-
import { extensionRelayServer } from "../browser-extension-relay/server.js";
|
|
15
20
|
import {
|
|
16
21
|
startGuardianActionSweep,
|
|
17
22
|
stopGuardianActionSweep,
|
|
@@ -63,17 +68,24 @@ import {
|
|
|
63
68
|
} from "../security/oauth-callback-registry.js";
|
|
64
69
|
import { UserError } from "../util/errors.js";
|
|
65
70
|
import { getLogger } from "../util/logger.js";
|
|
71
|
+
import { getRuntimePortFilePath } from "../util/platform.js";
|
|
66
72
|
import { buildAssistantEvent } from "./assistant-event.js";
|
|
67
73
|
import { assistantEventHub } from "./assistant-event-hub.js";
|
|
68
74
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
|
|
69
75
|
// Auth
|
|
70
|
-
import {
|
|
76
|
+
import {
|
|
77
|
+
authenticateHostBrowserResultRequest,
|
|
78
|
+
authenticateRequest,
|
|
79
|
+
} from "./auth/middleware.js";
|
|
80
|
+
import { parseSub } from "./auth/subject.js";
|
|
71
81
|
import {
|
|
72
82
|
mintDaemonDeliveryToken,
|
|
73
83
|
mintUiPageToken,
|
|
74
84
|
verifyToken,
|
|
75
85
|
} from "./auth/token-service.js";
|
|
86
|
+
import { verifyHostBrowserCapability } from "./capability-tokens.js";
|
|
76
87
|
import { sweepFailedEvents } from "./channel-retry-sweep.js";
|
|
88
|
+
import { getChromeExtensionRegistry } from "./chrome-extension-registry.js";
|
|
77
89
|
import { httpError } from "./http-errors.js";
|
|
78
90
|
import type { RouteDefinition } from "./http-router.js";
|
|
79
91
|
import { HttpRouter } from "./http-router.js";
|
|
@@ -110,6 +122,8 @@ import { attachmentRouteDefinitions } from "./routes/attachment-routes.js";
|
|
|
110
122
|
import { handleGetAudio } from "./routes/audio-routes.js";
|
|
111
123
|
import { avatarRouteDefinitions } from "./routes/avatar-routes.js";
|
|
112
124
|
import { brainGraphRouteDefinitions } from "./routes/brain-graph-routes.js";
|
|
125
|
+
import { browserCdpRouteDefinitions } from "./routes/browser-cdp-routes.js";
|
|
126
|
+
import { handleBrowserExtensionPair } from "./routes/browser-extension-pair-routes.js";
|
|
113
127
|
import { btwRouteDefinitions } from "./routes/btw-routes.js";
|
|
114
128
|
import { callRouteDefinitions } from "./routes/call-routes.js";
|
|
115
129
|
import {
|
|
@@ -147,6 +161,12 @@ import { handleGuardianBootstrap } from "./routes/guardian-bootstrap-routes.js";
|
|
|
147
161
|
import { handleGuardianRefresh } from "./routes/guardian-refresh-routes.js";
|
|
148
162
|
import { heartbeatRouteDefinitions } from "./routes/heartbeat-routes.js";
|
|
149
163
|
import { hostBashRouteDefinitions } from "./routes/host-bash-routes.js";
|
|
164
|
+
import {
|
|
165
|
+
hostBrowserRouteDefinitions,
|
|
166
|
+
resolveHostBrowserEvent,
|
|
167
|
+
resolveHostBrowserResultByRequestId,
|
|
168
|
+
resolveHostBrowserSessionInvalidated,
|
|
169
|
+
} from "./routes/host-browser-routes.js";
|
|
150
170
|
import { hostCuRouteDefinitions } from "./routes/host-cu-routes.js";
|
|
151
171
|
import { hostFileRouteDefinitions } from "./routes/host-file-routes.js";
|
|
152
172
|
import {
|
|
@@ -228,6 +248,40 @@ const DEFAULT_HOSTNAME = "127.0.0.1";
|
|
|
228
248
|
/** Global hard cap on request body size (512 MB — accommodates large .vbundle backup imports). */
|
|
229
249
|
const MAX_REQUEST_BODY_BYTES = 512 * 1024 * 1024;
|
|
230
250
|
|
|
251
|
+
/**
|
|
252
|
+
* WebSocket data attached to `/v1/browser-relay` connections. The route
|
|
253
|
+
* is used exclusively by the chrome-extension CDP proxy — outbound
|
|
254
|
+
* `host_browser_request` frames are pushed through the
|
|
255
|
+
* {@link ChromeExtensionRegistry}, and inbound `host_browser_result`
|
|
256
|
+
* frames are dispatched through
|
|
257
|
+
* `resolveHostBrowserResultByRequestId`. The extension may also submit
|
|
258
|
+
* results via `POST /v1/host-browser-result` (both transports resolve
|
|
259
|
+
* through the same core function).
|
|
260
|
+
*/
|
|
261
|
+
interface BrowserRelayWebSocketData {
|
|
262
|
+
wsType: "browser-relay";
|
|
263
|
+
connectionId: string;
|
|
264
|
+
/**
|
|
265
|
+
* Guardian identity derived from the JWT claims at WebSocket upgrade
|
|
266
|
+
* time. Used by the ChromeExtensionRegistry to route
|
|
267
|
+
* host_browser_request frames to the correct extension. Undefined when
|
|
268
|
+
* HTTP auth is disabled (dev bypass) or when the token's sub cannot be
|
|
269
|
+
* parsed into an actor principal.
|
|
270
|
+
*/
|
|
271
|
+
guardianId?: string;
|
|
272
|
+
/**
|
|
273
|
+
* Stable per-extension-install identifier supplied by the client on
|
|
274
|
+
* the WebSocket handshake (via the `clientInstanceId` query param or
|
|
275
|
+
* the `x-client-instance-id` header). Plumbed into the
|
|
276
|
+
* ChromeExtensionRegistry so multiple parallel installs for the same
|
|
277
|
+
* guardian (e.g. two Chrome profiles, two desktops) don't evict each
|
|
278
|
+
* other on register/unregister. Undefined on older extension builds
|
|
279
|
+
* — the registry synthesizes a connection-scoped fallback key in
|
|
280
|
+
* that case for backwards-compatible single-instance semantics.
|
|
281
|
+
*/
|
|
282
|
+
clientInstanceId?: string;
|
|
283
|
+
}
|
|
284
|
+
|
|
231
285
|
export class RuntimeHttpServer {
|
|
232
286
|
private server: ReturnType<typeof Bun.serve> | null = null;
|
|
233
287
|
private port: number;
|
|
@@ -329,9 +383,20 @@ export class RuntimeHttpServer {
|
|
|
329
383
|
open(ws) {
|
|
330
384
|
const data = ws.data as AllWebSocketData;
|
|
331
385
|
if ("wsType" in data && data.wsType === "browser-relay") {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
386
|
+
// When the JWT sub resolved to a guardian principal at upgrade
|
|
387
|
+
// time, register this connection with the chrome-extension
|
|
388
|
+
// registry so host_browser_request frames can be routed to it.
|
|
389
|
+
if (data.guardianId) {
|
|
390
|
+
const now = Date.now();
|
|
391
|
+
getChromeExtensionRegistry().register({
|
|
392
|
+
id: data.connectionId,
|
|
393
|
+
guardianId: data.guardianId,
|
|
394
|
+
clientInstanceId: data.clientInstanceId,
|
|
395
|
+
ws,
|
|
396
|
+
connectedAt: now,
|
|
397
|
+
lastActiveAt: now,
|
|
398
|
+
});
|
|
399
|
+
}
|
|
335
400
|
return;
|
|
336
401
|
}
|
|
337
402
|
const callSessionId = (data as RelayWebSocketData).callSessionId;
|
|
@@ -351,11 +416,119 @@ export class RuntimeHttpServer {
|
|
|
351
416
|
? message
|
|
352
417
|
: new TextDecoder().decode(message);
|
|
353
418
|
if ("wsType" in data && data.wsType === "browser-relay") {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
419
|
+
// Inbound frames on `/v1/browser-relay` carry one of:
|
|
420
|
+
// - `host_browser_result` — paired response to an outbound
|
|
421
|
+
// `host_browser_request` (see PR2).
|
|
422
|
+
// - `host_browser_event` — unsolicited CDP event forwarded
|
|
423
|
+
// from the extension's `chrome.debugger.onEvent`
|
|
424
|
+
// subscription (PR10).
|
|
425
|
+
// - `host_browser_session_invalidated` — detach
|
|
426
|
+
// notification forwarded from the extension's
|
|
427
|
+
// `chrome.debugger.onDetach` subscription (PR10).
|
|
428
|
+
//
|
|
429
|
+
// Every supported frame type delegates into a shared
|
|
430
|
+
// resolver exported from `host-browser-routes.ts` so the
|
|
431
|
+
// validation and resolution semantics stay in lockstep
|
|
432
|
+
// with the HTTP path. Malformed or unsupported frames are
|
|
433
|
+
// logged at debug and swallowed — we never throw out of a
|
|
434
|
+
// WebSocket `message` handler because an uncaught
|
|
435
|
+
// exception would tear down the whole socket for an
|
|
436
|
+
// attacker-controlled payload.
|
|
437
|
+
let parsed: unknown;
|
|
438
|
+
try {
|
|
439
|
+
parsed = JSON.parse(raw);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
log.debug(
|
|
442
|
+
{
|
|
443
|
+
connectionId: data.connectionId,
|
|
444
|
+
error: err instanceof Error ? err.message : String(err),
|
|
445
|
+
},
|
|
446
|
+
"browser-relay: dropped non-JSON inbound frame",
|
|
447
|
+
);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (!parsed || typeof parsed !== "object") {
|
|
451
|
+
log.debug(
|
|
452
|
+
{ connectionId: data.connectionId },
|
|
453
|
+
"browser-relay: dropped non-object inbound frame",
|
|
454
|
+
);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const frame = parsed as Record<string, unknown>;
|
|
458
|
+
switch (frame.type) {
|
|
459
|
+
case "host_browser_result": {
|
|
460
|
+
const resolution = resolveHostBrowserResultByRequestId({
|
|
461
|
+
requestId: frame.requestId,
|
|
462
|
+
content: frame.content,
|
|
463
|
+
isError: frame.isError,
|
|
464
|
+
});
|
|
465
|
+
if (!resolution.ok) {
|
|
466
|
+
log.warn(
|
|
467
|
+
{
|
|
468
|
+
connectionId: data.connectionId,
|
|
469
|
+
requestId:
|
|
470
|
+
typeof frame.requestId === "string"
|
|
471
|
+
? frame.requestId
|
|
472
|
+
: undefined,
|
|
473
|
+
code: resolution.code,
|
|
474
|
+
message: resolution.message,
|
|
475
|
+
},
|
|
476
|
+
"browser-relay: host_browser_result frame rejected",
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
case "host_browser_event": {
|
|
482
|
+
const resolution = resolveHostBrowserEvent({
|
|
483
|
+
method: frame.method,
|
|
484
|
+
params: frame.params,
|
|
485
|
+
cdpSessionId: frame.cdpSessionId,
|
|
486
|
+
});
|
|
487
|
+
if (!resolution.ok) {
|
|
488
|
+
log.warn(
|
|
489
|
+
{
|
|
490
|
+
connectionId: data.connectionId,
|
|
491
|
+
method:
|
|
492
|
+
typeof frame.method === "string"
|
|
493
|
+
? frame.method
|
|
494
|
+
: undefined,
|
|
495
|
+
code: resolution.code,
|
|
496
|
+
message: resolution.message,
|
|
497
|
+
},
|
|
498
|
+
"browser-relay: host_browser_event frame rejected",
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
case "host_browser_session_invalidated": {
|
|
504
|
+
const resolution = resolveHostBrowserSessionInvalidated({
|
|
505
|
+
targetId: frame.targetId,
|
|
506
|
+
reason: frame.reason,
|
|
507
|
+
});
|
|
508
|
+
if (!resolution.ok) {
|
|
509
|
+
log.warn(
|
|
510
|
+
{
|
|
511
|
+
connectionId: data.connectionId,
|
|
512
|
+
targetId:
|
|
513
|
+
typeof frame.targetId === "string"
|
|
514
|
+
? frame.targetId
|
|
515
|
+
: undefined,
|
|
516
|
+
code: resolution.code,
|
|
517
|
+
message: resolution.message,
|
|
518
|
+
},
|
|
519
|
+
"browser-relay: host_browser_session_invalidated frame rejected",
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
default: {
|
|
525
|
+
log.debug(
|
|
526
|
+
{ connectionId: data.connectionId, type: frame.type },
|
|
527
|
+
"browser-relay: dropped unsupported inbound frame type",
|
|
528
|
+
);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
359
532
|
}
|
|
360
533
|
const callSessionId = (data as RelayWebSocketData).callSessionId;
|
|
361
534
|
if (callSessionId) {
|
|
@@ -366,11 +539,12 @@ export class RuntimeHttpServer {
|
|
|
366
539
|
close(ws, code, reason) {
|
|
367
540
|
const data = ws.data as AllWebSocketData;
|
|
368
541
|
if ("wsType" in data && data.wsType === "browser-relay") {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
)
|
|
542
|
+
// Always attempt to unregister — the registry uses connectionId
|
|
543
|
+
// as the key and no-ops if the entry is absent (e.g. when the
|
|
544
|
+
// connection was never registered because guardianId was
|
|
545
|
+
// undefined, or when it was superseded by a newer registration
|
|
546
|
+
// for the same guardian).
|
|
547
|
+
getChromeExtensionRegistry().unregister(data.connectionId);
|
|
374
548
|
return;
|
|
375
549
|
}
|
|
376
550
|
const callSessionId = (data as RelayWebSocketData).callSessionId;
|
|
@@ -388,7 +562,51 @@ export class RuntimeHttpServer {
|
|
|
388
562
|
},
|
|
389
563
|
});
|
|
390
564
|
|
|
391
|
-
|
|
565
|
+
this.startBackgroundSweeps();
|
|
566
|
+
|
|
567
|
+
log.info(
|
|
568
|
+
"Running in gateway-only ingress mode. Direct webhook routes disabled.",
|
|
569
|
+
);
|
|
570
|
+
if (!isLoopbackHost(this.hostname)) {
|
|
571
|
+
log.warn(
|
|
572
|
+
"RUNTIME_HTTP_HOST is not bound to loopback. This may expose the runtime to direct public access.",
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
this.pairingStore.start();
|
|
577
|
+
|
|
578
|
+
if (hasUngatedHttpAuthDisabled()) {
|
|
579
|
+
log.warn(
|
|
580
|
+
"DISABLE_HTTP_AUTH is set but VELLUM_UNSAFE_AUTH_BYPASS=1 is not — auth bypass is IGNORED and HTTP authentication remains enabled. Set VELLUM_UNSAFE_AUTH_BYPASS=1 to confirm the bypass.",
|
|
581
|
+
);
|
|
582
|
+
} else if (isHttpAuthDisabled()) {
|
|
583
|
+
log.warn(
|
|
584
|
+
"DISABLE_HTTP_AUTH is set — HTTP API authentication is DISABLED. All API endpoints are accessible without a bearer token. Do not use in production.",
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
log.info(
|
|
589
|
+
{
|
|
590
|
+
port: this.actualPort,
|
|
591
|
+
hostname: this.hostname,
|
|
592
|
+
auth: !!this.bearerToken,
|
|
593
|
+
},
|
|
594
|
+
"Runtime HTTP server listening",
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
// Advertise the actual port to thin helpers that need to reach the
|
|
598
|
+
// runtime without inheriting the daemon's environment (e.g. the
|
|
599
|
+
// chrome-extension native messaging helper, spawned by Chrome).
|
|
600
|
+
this.writeRuntimePortFile(this.actualPort);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Start background sweep timers: retry sweep for failed channel events,
|
|
605
|
+
* guardian approval/action expiry sweeps, and canonical guardian expiry.
|
|
606
|
+
* Extracted from start() to allow future callers to defer sweep startup.
|
|
607
|
+
*/
|
|
608
|
+
private startBackgroundSweeps(): void {
|
|
609
|
+
if (this.processMessage && !this.retrySweepTimer) {
|
|
392
610
|
const pm = this.processMessage;
|
|
393
611
|
const mintBt = () => mintDaemonDeliveryToken();
|
|
394
612
|
this.retrySweepTimer = setInterval(() => {
|
|
@@ -416,36 +634,76 @@ export class RuntimeHttpServer {
|
|
|
416
634
|
|
|
417
635
|
startCanonicalGuardianExpirySweep();
|
|
418
636
|
log.info("Canonical guardian request expiry sweep started");
|
|
637
|
+
}
|
|
419
638
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
639
|
+
/**
|
|
640
|
+
* Atomically publish the runtime HTTP port to ~/.vellum/runtime-port so
|
|
641
|
+
* external helpers can locate a non-default `RUNTIME_HTTP_PORT` without
|
|
642
|
+
* any manifest changes. Best-effort — write failures never block
|
|
643
|
+
* daemon startup (see assistant/AGENTS.md "Daemon startup philosophy").
|
|
644
|
+
*/
|
|
645
|
+
private writeRuntimePortFile(actualPort: number): void {
|
|
646
|
+
try {
|
|
647
|
+
const portFile = getRuntimePortFilePath();
|
|
648
|
+
const dir = dirname(portFile);
|
|
649
|
+
if (!existsSync(dir)) {
|
|
650
|
+
mkdirSync(dir, { recursive: true });
|
|
651
|
+
}
|
|
652
|
+
const tmpPath = `${portFile}.tmp.${process.pid}`;
|
|
653
|
+
writeFileSync(tmpPath, String(actualPort), { mode: 0o644 });
|
|
654
|
+
renameSync(tmpPath, portFile);
|
|
655
|
+
log.info({ portFile, actualPort }, "Wrote runtime port file");
|
|
656
|
+
} catch (err) {
|
|
424
657
|
log.warn(
|
|
425
|
-
|
|
658
|
+
{ err },
|
|
659
|
+
"Failed to write runtime port file; non-default assistant ports may require --assistant-port on thin helpers",
|
|
426
660
|
);
|
|
427
661
|
}
|
|
662
|
+
}
|
|
428
663
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
664
|
+
/**
|
|
665
|
+
* Remove the runtime port file written by `writeRuntimePortFile`.
|
|
666
|
+
* Called from `stop()` on clean shutdown so a stale file does not
|
|
667
|
+
* point thin helpers (e.g. the chrome-extension native messaging
|
|
668
|
+
* helper) at a dead port until the next daemon start overwrites it.
|
|
669
|
+
* Best-effort — unlink failures never block shutdown.
|
|
670
|
+
*
|
|
671
|
+
* The unlink is conditional: we only remove the file if its current
|
|
672
|
+
* contents still match this server's port. The runtime-port file
|
|
673
|
+
* lives at the user-home level (`~/.vellum/runtime-port`) and is
|
|
674
|
+
* therefore shared across multiple daemon instances running on
|
|
675
|
+
* different `RUNTIME_HTTP_PORT`s. If a sibling instance has already
|
|
676
|
+
* rewritten the file with its own port, deleting it would strand
|
|
677
|
+
* thin helpers on the default port `7821` and break their ability
|
|
678
|
+
* to reach the still-running sibling.
|
|
679
|
+
*
|
|
680
|
+
* Note: this only runs on graceful shutdown. A crash leaves the
|
|
681
|
+
* file in place; the next successful startup overwrites it.
|
|
682
|
+
*/
|
|
683
|
+
private removeRuntimePortFile(): void {
|
|
684
|
+
try {
|
|
685
|
+
const portFile = getRuntimePortFilePath();
|
|
686
|
+
if (!existsSync(portFile)) return;
|
|
687
|
+
// Read-then-compare-then-unlink. Race-safe enough: the worst case
|
|
688
|
+
// is that another instance writes the file between our read and
|
|
689
|
+
// our unlink, in which case we erroneously delete its mapping.
|
|
690
|
+
// That window is short (a few microseconds) and a sibling startup
|
|
691
|
+
// will rewrite the file on its next port-publish call. The much
|
|
692
|
+
// more common multi-instance race — sibling already overwrote
|
|
693
|
+
// before our stop() runs — is correctly handled here as a no-op.
|
|
694
|
+
const current = readFileSync(portFile, "utf-8").trim();
|
|
695
|
+
if (current !== String(this.actualPort)) {
|
|
696
|
+
log.info(
|
|
697
|
+
{ portFile, current, actualPort: this.actualPort },
|
|
698
|
+
"Leaving runtime port file alone — owned by another instance",
|
|
699
|
+
);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
unlinkSync(portFile);
|
|
703
|
+
log.info({ portFile }, "Removed runtime port file");
|
|
704
|
+
} catch (err) {
|
|
705
|
+
log.warn({ err }, "Failed to remove runtime port file");
|
|
439
706
|
}
|
|
440
|
-
|
|
441
|
-
log.info(
|
|
442
|
-
{
|
|
443
|
-
port: this.actualPort,
|
|
444
|
-
hostname: this.hostname,
|
|
445
|
-
auth: !!this.bearerToken,
|
|
446
|
-
},
|
|
447
|
-
"Runtime HTTP server listening",
|
|
448
|
-
);
|
|
449
707
|
}
|
|
450
708
|
|
|
451
709
|
async stop(): Promise<void> {
|
|
@@ -462,6 +720,7 @@ export class RuntimeHttpServer {
|
|
|
462
720
|
this.server = null;
|
|
463
721
|
log.info("Runtime HTTP server stopped");
|
|
464
722
|
}
|
|
723
|
+
this.removeRuntimePortFile();
|
|
465
724
|
}
|
|
466
725
|
|
|
467
726
|
private async handleRequest(
|
|
@@ -532,6 +791,13 @@ export class RuntimeHttpServer {
|
|
|
532
791
|
return handlePairingStatus(url, this.pairingContext);
|
|
533
792
|
}
|
|
534
793
|
|
|
794
|
+
// Chrome extension capability-token pair endpoint — unauthenticated but
|
|
795
|
+
// restricted to loopback peers + an extension-id allowlist. Used by the
|
|
796
|
+
// native messaging helper to bootstrap a scoped token.
|
|
797
|
+
if (path === "/v1/browser-extension-pair") {
|
|
798
|
+
return await handleBrowserExtensionPair(req, server);
|
|
799
|
+
}
|
|
800
|
+
|
|
535
801
|
// Guardian bootstrap and refresh endpoints — before JWT auth because
|
|
536
802
|
// bootstrap is the first endpoint called to obtain a JWT, and refresh
|
|
537
803
|
// needs to work when the access token is expired. Bootstrap has its
|
|
@@ -546,7 +812,19 @@ export class RuntimeHttpServer {
|
|
|
546
812
|
|
|
547
813
|
// JWT bearer authentication — replaces the old shared-secret comparison.
|
|
548
814
|
// authenticateRequest handles dev bypass (DISABLE_HTTP_AUTH) internally.
|
|
549
|
-
|
|
815
|
+
//
|
|
816
|
+
// Special-case: /v1/host-browser-result POST accepts either a
|
|
817
|
+
// daemon-minted JWT (legacy/cloud) or a host_browser capability
|
|
818
|
+
// token (self-hosted chrome extension). The chrome extension's
|
|
819
|
+
// HTTP fallback (`postHostBrowserResult`) hands over the same
|
|
820
|
+
// capability token it presented to `/v1/browser-relay`, so the
|
|
821
|
+
// POST route must understand both auth shapes. Every other route
|
|
822
|
+
// keeps the JWT-only flow via `authenticateRequest`.
|
|
823
|
+
const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path;
|
|
824
|
+
const authResult =
|
|
825
|
+
normalizedPath === "/v1/host-browser-result" && req.method === "POST"
|
|
826
|
+
? authenticateHostBrowserResultRequest(req)
|
|
827
|
+
: authenticateRequest(req);
|
|
550
828
|
if (!authResult.ok) {
|
|
551
829
|
return authResult.response;
|
|
552
830
|
}
|
|
@@ -634,15 +912,110 @@ export class RuntimeHttpServer {
|
|
|
634
912
|
);
|
|
635
913
|
}
|
|
636
914
|
|
|
915
|
+
// When auth is enabled we accept two different kinds of token on the
|
|
916
|
+
// `/v1/browser-relay` handshake:
|
|
917
|
+
//
|
|
918
|
+
// 1. **Capability token** — a signed `host_browser_command`
|
|
919
|
+
// capability minted by `mintHostBrowserCapability()` and handed
|
|
920
|
+
// to the chrome extension by the native-messaging pair flow
|
|
921
|
+
// (`/v1/browser-extension-pair`). This is the preferred,
|
|
922
|
+
// self-hosted default: the extension never has to touch a
|
|
923
|
+
// gateway JWT.
|
|
924
|
+
// 2. **JWT** (audience `vellum-daemon`) — the legacy path used by
|
|
925
|
+
// the gateway-proxied cloud flow and by any compatibility
|
|
926
|
+
// callers that still hold a daemon-bound JWT. In that case we
|
|
927
|
+
// parse the JWT `sub` to extract the actor principal id and
|
|
928
|
+
// fall back to the explicit `x-guardian-id` / `guardianId`
|
|
929
|
+
// query param for service-token paths (see below).
|
|
930
|
+
//
|
|
931
|
+
// When auth is disabled (dev bypass), guardianId remains undefined
|
|
932
|
+
// and the registration is skipped — host_browser_request routing
|
|
933
|
+
// requires an authenticated guardian.
|
|
934
|
+
//
|
|
935
|
+
// Gateway path: when the WebSocket upgrade is proxied through the
|
|
936
|
+
// gateway, the upstream token minted by `mintServiceToken()` has
|
|
937
|
+
// `sub=svc:gateway:self` with no actor principal id. The gateway
|
|
938
|
+
// parses the downstream edge token's `actorPrincipalId` and forwards
|
|
939
|
+
// it as an explicit `guardianId` query parameter (and/or header) so
|
|
940
|
+
// we can register the connection under the real guardian. Missing
|
|
941
|
+
// guardian context on this path is rejected (fail closed).
|
|
942
|
+
// Read the client-supplied stable instance id off the handshake.
|
|
943
|
+
// The extension generates this on first run and persists it in
|
|
944
|
+
// chrome.storage so it survives service-worker restarts and
|
|
945
|
+
// browser restarts. The header form is preferred so gateway
|
|
946
|
+
// forwarding and proxy logs don't surface instance ids in the
|
|
947
|
+
// URL, but we also accept a query param for fetch-based clients
|
|
948
|
+
// that can't mutate headers. An empty string is treated as absent
|
|
949
|
+
// so sparse clients don't end up all sharing the same legacy key.
|
|
950
|
+
const rawInstanceHeader = req.headers.get("x-client-instance-id")?.trim();
|
|
951
|
+
const rawInstanceQuery = new URL(req.url).searchParams
|
|
952
|
+
.get("clientInstanceId")
|
|
953
|
+
?.trim();
|
|
954
|
+
const clientInstanceId =
|
|
955
|
+
(rawInstanceHeader ?? "") || (rawInstanceQuery ?? "") || undefined;
|
|
956
|
+
|
|
957
|
+
let guardianId: string | undefined;
|
|
637
958
|
if (!isHttpAuthDisabled()) {
|
|
638
959
|
const wsUrl = new URL(req.url);
|
|
639
960
|
const token = wsUrl.searchParams.get("token");
|
|
640
961
|
if (!token) {
|
|
641
962
|
return httpError("UNAUTHORIZED", "Unauthorized", 401);
|
|
642
963
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
964
|
+
// 1) Capability-token path (self-hosted default). The chrome
|
|
965
|
+
// extension presents the token it received from the native
|
|
966
|
+
// messaging pair flow. We derive `guardianId` from the
|
|
967
|
+
// capability claims directly — the claims are HMAC-signed by
|
|
968
|
+
// the same daemon so there is no cross-tenant risk.
|
|
969
|
+
const capabilityClaims = verifyHostBrowserCapability(token);
|
|
970
|
+
if (capabilityClaims) {
|
|
971
|
+
guardianId = capabilityClaims.guardianId;
|
|
972
|
+
} else {
|
|
973
|
+
// 2) JWT compatibility path (gateway / legacy). Fall back to the
|
|
974
|
+
// existing verifyToken+parseSub flow so cloud callers and any
|
|
975
|
+
// old self-hosted clients still holding a daemon JWT
|
|
976
|
+
// continue to work during the cutover.
|
|
977
|
+
const jwtResult = verifyToken(token, "vellum-daemon");
|
|
978
|
+
if (!jwtResult.ok) {
|
|
979
|
+
return httpError("UNAUTHORIZED", "Unauthorized", 401);
|
|
980
|
+
}
|
|
981
|
+
const subResult = parseSub(jwtResult.claims.sub);
|
|
982
|
+
if (subResult.ok && subResult.actorPrincipalId) {
|
|
983
|
+
// Direct actor principal — this is the loopback / desktop path.
|
|
984
|
+
guardianId = subResult.actorPrincipalId;
|
|
985
|
+
} else {
|
|
986
|
+
// Service-token path (gateway-forwarded). The gateway must plumb
|
|
987
|
+
// the resolved actor principal as an explicit `x-guardian-id`
|
|
988
|
+
// header or `guardianId` query param. Header takes precedence
|
|
989
|
+
// because headers are easier for the gateway to forward without
|
|
990
|
+
// rewriting the URL.
|
|
991
|
+
const headerGuardianId =
|
|
992
|
+
req.headers.get("x-guardian-id")?.trim() ?? "";
|
|
993
|
+
const queryGuardianId =
|
|
994
|
+
wsUrl.searchParams.get("guardianId")?.trim() ?? "";
|
|
995
|
+
const fallbackGuardianId = headerGuardianId || queryGuardianId;
|
|
996
|
+
if (fallbackGuardianId) {
|
|
997
|
+
guardianId = fallbackGuardianId;
|
|
998
|
+
} else {
|
|
999
|
+
// Fail closed: a service-token relay upgrade without a
|
|
1000
|
+
// guardian context cannot be routed safely. Allowing the
|
|
1001
|
+
// upgrade to proceed creates an unscoped socket that never
|
|
1002
|
+
// registers in the ChromeExtensionRegistry.
|
|
1003
|
+
log.warn(
|
|
1004
|
+
{
|
|
1005
|
+
principalType: subResult.ok
|
|
1006
|
+
? subResult.principalType
|
|
1007
|
+
: "unknown",
|
|
1008
|
+
sub: jwtResult.claims.sub,
|
|
1009
|
+
},
|
|
1010
|
+
"Browser relay upgrade denied: missing guardian context on service-token path",
|
|
1011
|
+
);
|
|
1012
|
+
return httpError(
|
|
1013
|
+
"UNAUTHORIZED",
|
|
1014
|
+
"Browser relay requires guardian context",
|
|
1015
|
+
401,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
646
1019
|
}
|
|
647
1020
|
}
|
|
648
1021
|
|
|
@@ -651,6 +1024,8 @@ export class RuntimeHttpServer {
|
|
|
651
1024
|
data: {
|
|
652
1025
|
wsType: "browser-relay",
|
|
653
1026
|
connectionId,
|
|
1027
|
+
guardianId,
|
|
1028
|
+
clientInstanceId,
|
|
654
1029
|
} satisfies BrowserRelayWebSocketData,
|
|
655
1030
|
});
|
|
656
1031
|
if (!upgraded) {
|
|
@@ -833,6 +1208,7 @@ export class RuntimeHttpServer {
|
|
|
833
1208
|
lastMessageAt: conversation.lastMessageAt,
|
|
834
1209
|
conversationType: conversation.conversationType ?? "standard",
|
|
835
1210
|
source: conversation.source ?? "user",
|
|
1211
|
+
hostAccess: conversation.hostAccess === 1,
|
|
836
1212
|
...(conversation.scheduleJobId
|
|
837
1213
|
? { scheduleJobId: conversation.scheduleJobId }
|
|
838
1214
|
: {}),
|
|
@@ -997,29 +1373,6 @@ export class RuntimeHttpServer {
|
|
|
997
1373
|
}),
|
|
998
1374
|
...ttsRouteDefinitions(),
|
|
999
1375
|
|
|
1000
|
-
// Browser relay — not extracted into a domain module because
|
|
1001
|
-
// these two routes depend on the in-process extensionRelayServer
|
|
1002
|
-
// singleton which is only available here.
|
|
1003
|
-
{
|
|
1004
|
-
endpoint: "browser-relay/status",
|
|
1005
|
-
method: "GET",
|
|
1006
|
-
handler: () => Response.json(extensionRelayServer.getStatus()),
|
|
1007
|
-
},
|
|
1008
|
-
{
|
|
1009
|
-
endpoint: "browser-relay/command",
|
|
1010
|
-
method: "POST",
|
|
1011
|
-
handler: async ({ req }) => {
|
|
1012
|
-
const body = (await req.json()) as Record<string, unknown>;
|
|
1013
|
-
const resp = await extensionRelayServer.sendCommand(
|
|
1014
|
-
body as Omit<
|
|
1015
|
-
import("../browser-extension-relay/protocol.js").ExtensionCommand,
|
|
1016
|
-
"id"
|
|
1017
|
-
>,
|
|
1018
|
-
);
|
|
1019
|
-
return Response.json(resp);
|
|
1020
|
-
},
|
|
1021
|
-
},
|
|
1022
|
-
|
|
1023
1376
|
// Conversation list and seen signal — kept inline because they
|
|
1024
1377
|
// depend on multiple cross-cutting stores that aren't grouped
|
|
1025
1378
|
// into a single domain module.
|
|
@@ -1209,6 +1562,8 @@ export class RuntimeHttpServer {
|
|
|
1209
1562
|
...globalSearchRouteDefinitions(),
|
|
1210
1563
|
...approvalRouteDefinitions(),
|
|
1211
1564
|
...hostBashRouteDefinitions(),
|
|
1565
|
+
...hostBrowserRouteDefinitions(),
|
|
1566
|
+
...browserCdpRouteDefinitions(),
|
|
1212
1567
|
...hostCuRouteDefinitions(),
|
|
1213
1568
|
...hostFileRouteDefinitions(),
|
|
1214
1569
|
...(this.getSkillContext
|