@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
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { isHttpAuthDisabled } from "../../config/env.js";
|
|
26
26
|
import { getLogger } from "../../util/logger.js";
|
|
27
27
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from "../assistant-scope.js";
|
|
28
|
+
import { verifyHostBrowserCapability } from "../capability-tokens.js";
|
|
28
29
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
29
30
|
import { buildAuthContext } from "./context.js";
|
|
30
31
|
import { resolveScopeProfile } from "./scopes.js";
|
|
@@ -186,3 +187,100 @@ export function authenticateRequest(req: Request): AuthenticateResult {
|
|
|
186
187
|
|
|
187
188
|
return { ok: true, context: contextResult.context };
|
|
188
189
|
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Capability-token-aware auth for /v1/host-browser-result
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build a synthetic AuthContext from a verified host_browser capability
|
|
197
|
+
* claim. The resulting context is shaped to look like an
|
|
198
|
+
* `actor_client_v1` actor so downstream route policy (which requires
|
|
199
|
+
* `approval.write`) and `requireBoundGuardian` (which compares
|
|
200
|
+
* `actorPrincipalId` against the bound guardian) both accept it.
|
|
201
|
+
*
|
|
202
|
+
* The capability token already carries its own HMAC-checked expiry, so
|
|
203
|
+
* there is no policy-epoch gate to apply here — we pin `policyEpoch` to
|
|
204
|
+
* `Number.MAX_SAFE_INTEGER` the same way the dev-bypass context does.
|
|
205
|
+
*/
|
|
206
|
+
function buildCapabilityAuthContext(guardianId: string): AuthContext {
|
|
207
|
+
return {
|
|
208
|
+
subject: `actor:${DAEMON_INTERNAL_ASSISTANT_ID}:${guardianId}`,
|
|
209
|
+
principalType: "actor",
|
|
210
|
+
assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
|
|
211
|
+
actorPrincipalId: guardianId,
|
|
212
|
+
scopeProfile: "actor_client_v1",
|
|
213
|
+
scopes: resolveScopeProfile("actor_client_v1"),
|
|
214
|
+
policyEpoch: Number.MAX_SAFE_INTEGER,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Authenticate a request that is allowed to present either a JWT or a
|
|
220
|
+
* host_browser capability token. This is the auth entry point for
|
|
221
|
+
* `/v1/host-browser-result` POST specifically — the chrome extension
|
|
222
|
+
* stores a capability token (minted by the
|
|
223
|
+
* `/v1/browser-extension-pair` flow) rather than a daemon JWT, so the
|
|
224
|
+
* POST fallback used when the `/v1/browser-relay` WebSocket is
|
|
225
|
+
* unavailable would otherwise 401 through the JWT-only
|
|
226
|
+
* `authenticateRequest` path.
|
|
227
|
+
*
|
|
228
|
+
* Order of operations (mirrors `handleBrowserRelayUpgrade`):
|
|
229
|
+
* 1. Extract the bearer token. Missing header → 401.
|
|
230
|
+
* 2. Try `verifyHostBrowserCapability(token)` first. If it succeeds,
|
|
231
|
+
* derive `guardianId` from the capability claims and synthesize an
|
|
232
|
+
* AuthContext.
|
|
233
|
+
* 3. Otherwise fall through to the standard JWT path so daemon-minted
|
|
234
|
+
* JWTs (gateway-proxied or direct) continue to work as a
|
|
235
|
+
* regression-safe compatibility path.
|
|
236
|
+
*
|
|
237
|
+
* Dev bypass (`isHttpAuthDisabled()`) is honored the same way as
|
|
238
|
+
* `authenticateRequest` — we delegate to it directly to pick up the
|
|
239
|
+
* shared synthetic dev-bypass context.
|
|
240
|
+
*/
|
|
241
|
+
export function authenticateHostBrowserResultRequest(
|
|
242
|
+
req: Request,
|
|
243
|
+
): AuthenticateResult {
|
|
244
|
+
if (isHttpAuthDisabled()) {
|
|
245
|
+
return { ok: true, context: buildDevBypassContext() };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const rawToken = extractBearerToken(req);
|
|
249
|
+
if (!rawToken) {
|
|
250
|
+
log.warn(
|
|
251
|
+
{ reason: "missing_token", path: "/v1/host-browser-result" },
|
|
252
|
+
"Host browser result auth denied: missing Authorization header",
|
|
253
|
+
);
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
response: Response.json(
|
|
257
|
+
{
|
|
258
|
+
error: {
|
|
259
|
+
code: "UNAUTHORIZED",
|
|
260
|
+
message: "Missing Authorization header",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
{ status: 401 },
|
|
264
|
+
),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 1) Capability-token path (self-hosted default). The chrome
|
|
269
|
+
// extension presents the token it received from the native
|
|
270
|
+
// messaging pair flow. We derive `actorPrincipalId` from the
|
|
271
|
+
// capability claims directly — the claims are HMAC-signed by the
|
|
272
|
+
// same daemon so there is no cross-tenant risk.
|
|
273
|
+
const capabilityClaims = verifyHostBrowserCapability(rawToken);
|
|
274
|
+
if (capabilityClaims) {
|
|
275
|
+
return {
|
|
276
|
+
ok: true,
|
|
277
|
+
context: buildCapabilityAuthContext(capabilityClaims.guardianId),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 2) JWT compatibility path. Fall back to the existing daemon/gateway
|
|
282
|
+
// JWT verification so cloud callers and any legacy self-hosted
|
|
283
|
+
// clients still holding a daemon JWT continue to work. Any 401
|
|
284
|
+
// emitted here already includes the JWT-specific reason.
|
|
285
|
+
return authenticateRequest(req);
|
|
286
|
+
}
|
|
@@ -133,6 +133,8 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
133
133
|
{ endpoint: "conversations/analyze", scopes: ["chat.write"] },
|
|
134
134
|
{ endpoint: "conversations/switch", scopes: ["chat.write"] },
|
|
135
135
|
{ endpoint: "conversations/name", scopes: ["chat.write"] },
|
|
136
|
+
{ endpoint: "conversations/host-access:GET", scopes: ["chat.read"] },
|
|
137
|
+
{ endpoint: "conversations/host-access", scopes: ["approval.write"] },
|
|
136
138
|
{ endpoint: "conversations/cancel", scopes: ["chat.write"] },
|
|
137
139
|
{ endpoint: "conversations/undo", scopes: ["chat.write"] },
|
|
138
140
|
{ endpoint: "conversations/regenerate", scopes: ["chat.write"] },
|
|
@@ -148,6 +150,7 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
148
150
|
{ endpoint: "secret", scopes: ["approval.write"] },
|
|
149
151
|
{ endpoint: "trust-rules", scopes: ["approval.write"] },
|
|
150
152
|
{ endpoint: "host-bash-result", scopes: ["approval.write"] },
|
|
153
|
+
{ endpoint: "host-browser-result", scopes: ["approval.write"] },
|
|
151
154
|
{ endpoint: "host-cu-result", scopes: ["approval.write"] },
|
|
152
155
|
{ endpoint: "host-file-result", scopes: ["approval.write"] },
|
|
153
156
|
{ endpoint: "pending-interactions", scopes: ["approval.read"] },
|
|
@@ -381,10 +384,6 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
381
384
|
// Queued message deletion
|
|
382
385
|
{ endpoint: "messages/queued", scopes: ["chat.write"] },
|
|
383
386
|
|
|
384
|
-
// Browser relay
|
|
385
|
-
{ endpoint: "browser-relay/status", scopes: ["settings.read"] },
|
|
386
|
-
{ endpoint: "browser-relay/command", scopes: ["settings.write"] },
|
|
387
|
-
|
|
388
387
|
// Interfaces
|
|
389
388
|
{ endpoint: "interfaces", scopes: ["settings.read"] },
|
|
390
389
|
|
|
@@ -482,9 +481,9 @@ const ACTOR_ENDPOINTS: Array<{ endpoint: string; scopes: Scope[] }> = [
|
|
|
482
481
|
{ endpoint: "tools", scopes: ["settings.read"] },
|
|
483
482
|
{ endpoint: "tools/simulate-permission", scopes: ["settings.read"] },
|
|
484
483
|
|
|
485
|
-
//
|
|
486
|
-
|
|
487
|
-
{ endpoint: "
|
|
484
|
+
// Browser CDP shim — backs the `assistant browser chrome relay` CLI used
|
|
485
|
+
// by the in-tree Amazon and Influencer skills.
|
|
486
|
+
{ endpoint: "browser-cdp", scopes: ["settings.write"] },
|
|
488
487
|
];
|
|
489
488
|
|
|
490
489
|
for (const { endpoint, scopes } of ACTOR_ENDPOINTS) {
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability token minting and verification for scoped, short-lived tokens
|
|
3
|
+
* issued to the chrome extension (and other thin clients) so they can submit
|
|
4
|
+
* results back to the runtime without a full guardian-bound JWT.
|
|
5
|
+
*
|
|
6
|
+
* Design:
|
|
7
|
+
* - Tokens are HMAC-SHA256 signed over a JSON claims payload.
|
|
8
|
+
* - Claims include a bound capability, guardian id, nonce, and expiry.
|
|
9
|
+
* - Signing uses a long-lived random secret persisted to
|
|
10
|
+
* `~/.vellum/protected/` with 0600 permissions. The protected
|
|
11
|
+
* directory sits outside the workspace per AGENTS.md: workspace
|
|
12
|
+
* directories must not hold security-sensitive material.
|
|
13
|
+
* - The secret is generated once on first launch and reused across
|
|
14
|
+
* subsequent daemon restarts so previously-minted tokens still verify.
|
|
15
|
+
* - Tests inject their own secret via `setCapabilityTokenSecretForTests`.
|
|
16
|
+
*
|
|
17
|
+
* The encoded token format is `<base64url(payload)>.<base64url(sig)>`.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
21
|
+
import {
|
|
22
|
+
chmodSync,
|
|
23
|
+
existsSync,
|
|
24
|
+
mkdirSync,
|
|
25
|
+
readFileSync,
|
|
26
|
+
renameSync,
|
|
27
|
+
unlinkSync,
|
|
28
|
+
writeFileSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { dirname, join } from "node:path";
|
|
32
|
+
|
|
33
|
+
import { getLogger } from "../util/logger.js";
|
|
34
|
+
import { getDataDir, getProtectedDir } from "../util/platform.js";
|
|
35
|
+
|
|
36
|
+
const log = getLogger("capability-tokens");
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Types
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/** Capability identifiers that can be bound to a capability token. */
|
|
43
|
+
export type Capability = "host_browser_command";
|
|
44
|
+
|
|
45
|
+
/** Claims encoded in the signed payload. */
|
|
46
|
+
export interface CapabilityClaims {
|
|
47
|
+
capability: Capability;
|
|
48
|
+
guardianId: string;
|
|
49
|
+
/** 16-byte random nonce, hex-encoded. Prevents replay across fresh mints. */
|
|
50
|
+
nonce: string;
|
|
51
|
+
/** ms-since-epoch expiry. */
|
|
52
|
+
expiresAt: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A freshly-minted capability token and its absolute expiry. */
|
|
56
|
+
export interface CapabilityToken {
|
|
57
|
+
token: string;
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Secret lifecycle
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
let _secret: Buffer | undefined;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the canonical path where the capability-token secret is
|
|
69
|
+
* persisted: `~/.vellum/protected/capability-token-secret`. The protected
|
|
70
|
+
* directory is the canonical location for security-sensitive material
|
|
71
|
+
* and sits outside the workspace (which AGENTS.md forbids for secrets).
|
|
72
|
+
*/
|
|
73
|
+
function getSecretPath(): string {
|
|
74
|
+
return join(getProtectedDir(), "capability-token-secret");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Legacy path under `workspace/data/` where earlier builds persisted the
|
|
79
|
+
* capability-token secret. We keep this as a read-only migration source
|
|
80
|
+
* so existing deployments don't regenerate their secret (and invalidate
|
|
81
|
+
* every outstanding token) on upgrade — the first launch after the
|
|
82
|
+
* upgrade copies the legacy file into `getProtectedDir()` and removes it
|
|
83
|
+
* from the workspace.
|
|
84
|
+
*/
|
|
85
|
+
function getLegacySecretPath(): string {
|
|
86
|
+
return join(getDataDir(), "capability-token-secret");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Path overrides used by unit tests to drive the secret lifecycle
|
|
91
|
+
* without touching the real `~/.vellum/` tree. Production callers must
|
|
92
|
+
* omit this argument so the canonical paths (`getProtectedDir()` +
|
|
93
|
+
* `getDataDir()`) are used.
|
|
94
|
+
*/
|
|
95
|
+
export interface CapabilityTokenSecretPaths {
|
|
96
|
+
/** Protected-directory secret path (authoritative). */
|
|
97
|
+
secretPath: string;
|
|
98
|
+
/** Legacy workspace-directory secret path (migration source). */
|
|
99
|
+
legacySecretPath: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load the capability-token secret from disk or generate and persist a new
|
|
104
|
+
* one. Atomically writes with mode 0o600 so the secret is not readable by
|
|
105
|
+
* other users on the same host.
|
|
106
|
+
*
|
|
107
|
+
* Migration: if the secret exists only at the legacy workspace path, copy
|
|
108
|
+
* it into the protected directory and delete the workspace copy so we do
|
|
109
|
+
* not leave security-sensitive material inside `workspace/`.
|
|
110
|
+
*
|
|
111
|
+
* The optional `paths` argument is for unit tests only — production
|
|
112
|
+
* callers must omit it and use the canonical `~/.vellum/protected/` /
|
|
113
|
+
* `~/.vellum/workspace/data/` paths.
|
|
114
|
+
*/
|
|
115
|
+
export function loadOrCreateCapabilityTokenSecret(
|
|
116
|
+
paths?: CapabilityTokenSecretPaths,
|
|
117
|
+
): Buffer {
|
|
118
|
+
const keyPath = paths?.secretPath ?? getSecretPath();
|
|
119
|
+
const legacyPath = paths?.legacySecretPath ?? getLegacySecretPath();
|
|
120
|
+
if (existsSync(keyPath)) {
|
|
121
|
+
try {
|
|
122
|
+
const raw = readFileSync(keyPath);
|
|
123
|
+
if (raw.length === 32) {
|
|
124
|
+
return raw;
|
|
125
|
+
}
|
|
126
|
+
log.warn(
|
|
127
|
+
{ keyPath, length: raw.length },
|
|
128
|
+
"capability token secret has unexpected length — regenerating",
|
|
129
|
+
);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
log.warn(
|
|
132
|
+
{ err, keyPath },
|
|
133
|
+
"Failed to read capability token secret — regenerating",
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Attempt to migrate a legacy workspace-directory secret before we
|
|
139
|
+
// generate a fresh one. If this succeeds we end up with the legacy
|
|
140
|
+
// secret persisted at the protected path and the workspace copy
|
|
141
|
+
// removed, preserving every outstanding token across the upgrade.
|
|
142
|
+
const migrated = migrateLegacyCapabilityTokenSecret(keyPath, legacyPath);
|
|
143
|
+
if (migrated) {
|
|
144
|
+
return migrated;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const fresh = randomBytes(32);
|
|
148
|
+
writeSecretAtomic(keyPath, fresh);
|
|
149
|
+
log.info("Capability token secret generated and persisted");
|
|
150
|
+
return fresh;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Write `secret` to `keyPath` atomically with mode 0o600. Ensures the
|
|
155
|
+
* parent directory exists.
|
|
156
|
+
*/
|
|
157
|
+
function writeSecretAtomic(keyPath: string, secret: Buffer): void {
|
|
158
|
+
const dir = dirname(keyPath);
|
|
159
|
+
if (!existsSync(dir)) {
|
|
160
|
+
mkdirSync(dir, { recursive: true });
|
|
161
|
+
}
|
|
162
|
+
const tmpPath = `${keyPath}.tmp.${process.pid}`;
|
|
163
|
+
writeFileSync(tmpPath, secret, { mode: 0o600 });
|
|
164
|
+
renameSync(tmpPath, keyPath);
|
|
165
|
+
try {
|
|
166
|
+
chmodSync(keyPath, 0o600);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
log.warn(
|
|
169
|
+
{ err, keyPath },
|
|
170
|
+
"Failed to chmod capability token secret after write",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* If a pre-migration capability token secret exists under the workspace
|
|
177
|
+
* data directory, copy it into the protected directory and remove the
|
|
178
|
+
* workspace copy. Returns the migrated secret if migration ran
|
|
179
|
+
* successfully, or `undefined` if there was nothing to migrate or the
|
|
180
|
+
* migration failed.
|
|
181
|
+
*/
|
|
182
|
+
function migrateLegacyCapabilityTokenSecret(
|
|
183
|
+
secretPath: string,
|
|
184
|
+
legacyPath: string,
|
|
185
|
+
): Buffer | undefined {
|
|
186
|
+
if (!existsSync(legacyPath)) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
const raw = readFileSync(legacyPath);
|
|
191
|
+
if (raw.length !== 32) {
|
|
192
|
+
log.warn(
|
|
193
|
+
{ legacyPath, length: raw.length },
|
|
194
|
+
"legacy capability token secret has unexpected length — ignoring",
|
|
195
|
+
);
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
writeSecretAtomic(secretPath, raw);
|
|
199
|
+
try {
|
|
200
|
+
unlinkSync(legacyPath);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
log.warn(
|
|
203
|
+
{ err, legacyPath },
|
|
204
|
+
"Failed to remove legacy workspace capability token secret after migration",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
log.info(
|
|
208
|
+
{ from: legacyPath, to: secretPath },
|
|
209
|
+
"Migrated capability token secret out of workspace into protected directory",
|
|
210
|
+
);
|
|
211
|
+
return raw;
|
|
212
|
+
} catch (err) {
|
|
213
|
+
log.warn(
|
|
214
|
+
{ err, legacyPath },
|
|
215
|
+
"Failed to migrate legacy capability token secret — regenerating",
|
|
216
|
+
);
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Initialize the module-level secret. Called once at daemon startup. Safe
|
|
223
|
+
* to call multiple times — subsequent calls overwrite the cached value
|
|
224
|
+
* (useful in tests that reset state).
|
|
225
|
+
*/
|
|
226
|
+
export function initCapabilityTokenSecret(secret: Buffer): void {
|
|
227
|
+
if (secret.length !== 32) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`capability token secret must be 32 bytes, got ${secret.length}`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
_secret = secret;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Test-only helper to inject a deterministic secret.
|
|
237
|
+
*/
|
|
238
|
+
export function setCapabilityTokenSecretForTests(secret: Buffer): void {
|
|
239
|
+
_secret = secret;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Reset the cached secret. Test-only — exposed so test isolation can
|
|
244
|
+
* force a reload from disk.
|
|
245
|
+
*/
|
|
246
|
+
export function resetCapabilityTokenSecretForTests(): void {
|
|
247
|
+
_secret = undefined;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function getSecret(): Buffer {
|
|
251
|
+
if (_secret) return _secret;
|
|
252
|
+
if (process.env.NODE_ENV === "test") {
|
|
253
|
+
_secret = randomBytes(32);
|
|
254
|
+
return _secret;
|
|
255
|
+
}
|
|
256
|
+
// Lazy load — daemon startup is expected to call
|
|
257
|
+
// `initCapabilityTokenSecret(loadOrCreateCapabilityTokenSecret())` but
|
|
258
|
+
// we fall back to a disk load here so unit tests and early call sites
|
|
259
|
+
// don't have to depend on startup ordering.
|
|
260
|
+
_secret = loadOrCreateCapabilityTokenSecret();
|
|
261
|
+
return _secret;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// Mint / verify
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
const DEFAULT_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
269
|
+
|
|
270
|
+
function base64urlEncode(buf: Buffer): string {
|
|
271
|
+
return buf
|
|
272
|
+
.toString("base64")
|
|
273
|
+
.replace(/\+/g, "-")
|
|
274
|
+
.replace(/\//g, "_")
|
|
275
|
+
.replace(/=+$/, "");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function base64urlDecode(s: string): Buffer {
|
|
279
|
+
const pad = s.length % 4 === 0 ? 0 : 4 - (s.length % 4);
|
|
280
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(pad);
|
|
281
|
+
return Buffer.from(b64, "base64");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function sign(payload: string, secret: Buffer): string {
|
|
285
|
+
return base64urlEncode(createHmac("sha256", secret).update(payload).digest());
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Mint a capability token bound to the `host_browser_command` capability
|
|
290
|
+
* for the given guardian id. Default TTL is 30 minutes.
|
|
291
|
+
*/
|
|
292
|
+
export function mintHostBrowserCapability(
|
|
293
|
+
guardianId: string,
|
|
294
|
+
ttlMs: number = DEFAULT_TTL_MS,
|
|
295
|
+
): CapabilityToken {
|
|
296
|
+
const expiresAt = Date.now() + ttlMs;
|
|
297
|
+
const nonce = randomBytes(16).toString("hex");
|
|
298
|
+
const claims: CapabilityClaims = {
|
|
299
|
+
capability: "host_browser_command",
|
|
300
|
+
guardianId,
|
|
301
|
+
nonce,
|
|
302
|
+
expiresAt,
|
|
303
|
+
};
|
|
304
|
+
const payload = base64urlEncode(Buffer.from(JSON.stringify(claims), "utf8"));
|
|
305
|
+
const sig = sign(payload, getSecret());
|
|
306
|
+
return { token: `${payload}.${sig}`, expiresAt };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Verify a capability token minted by `mintHostBrowserCapability`.
|
|
311
|
+
*
|
|
312
|
+
* Returns the decoded claims on success or null if the signature is
|
|
313
|
+
* invalid, the payload is malformed, the token has expired, or the bound
|
|
314
|
+
* capability is not `host_browser_command`.
|
|
315
|
+
*
|
|
316
|
+
* Signature comparison uses `timingSafeEqual` to avoid leaking the secret
|
|
317
|
+
* through timing side channels.
|
|
318
|
+
*
|
|
319
|
+
* The `/v1/browser-relay` WebSocket upgrade handler in `http-server.ts`
|
|
320
|
+
* (`handleBrowserRelayUpgrade`) calls this to authenticate self-hosted
|
|
321
|
+
* chrome extensions on the capability-token branch before falling
|
|
322
|
+
* through to the JWT compatibility path. The `/v1/host-browser-result`
|
|
323
|
+
* POST route may also call it (see that route's auth handling) when a
|
|
324
|
+
* result is posted back with a capability-token bearer instead of a
|
|
325
|
+
* guardian-bound JWT.
|
|
326
|
+
*/
|
|
327
|
+
export function verifyHostBrowserCapability(
|
|
328
|
+
token: string,
|
|
329
|
+
): CapabilityClaims | null {
|
|
330
|
+
if (typeof token !== "string") return null;
|
|
331
|
+
const dot = token.indexOf(".");
|
|
332
|
+
if (dot < 0) return null;
|
|
333
|
+
const payload = token.slice(0, dot);
|
|
334
|
+
const sig = token.slice(dot + 1);
|
|
335
|
+
if (!payload || !sig) return null;
|
|
336
|
+
|
|
337
|
+
const expected = sign(payload, getSecret());
|
|
338
|
+
const a = Buffer.from(sig, "utf8");
|
|
339
|
+
const b = Buffer.from(expected, "utf8");
|
|
340
|
+
if (a.length !== b.length) return null;
|
|
341
|
+
if (!timingSafeEqual(a, b)) return null;
|
|
342
|
+
|
|
343
|
+
let claims: CapabilityClaims;
|
|
344
|
+
try {
|
|
345
|
+
claims = JSON.parse(
|
|
346
|
+
base64urlDecode(payload).toString("utf8"),
|
|
347
|
+
) as CapabilityClaims;
|
|
348
|
+
} catch {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!claims || typeof claims !== "object") return null;
|
|
353
|
+
if (claims.capability !== "host_browser_command") return null;
|
|
354
|
+
if (typeof claims.guardianId !== "string" || claims.guardianId.length === 0) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
if (typeof claims.nonce !== "string" || claims.nonce.length === 0) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
if (typeof claims.expiresAt !== "number" || claims.expiresAt <= Date.now()) {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return claims;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// Dev-only fallback token file
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Path to the dev-pairing fallback token file. The runtime writes a freshly
|
|
372
|
+
* minted capability token to this location on daemon startup so developers
|
|
373
|
+
* can manually pair the chrome extension without wiring the native
|
|
374
|
+
* messaging helper. Production users should pair via the native helper
|
|
375
|
+
* (PRs 7/12/13).
|
|
376
|
+
*/
|
|
377
|
+
export function getDaemonTokenFilePath(): string {
|
|
378
|
+
// Always under `~/.vellum/` (not the configurable workspace dir) so the
|
|
379
|
+
// native messaging helper can find it at a fixed path regardless of
|
|
380
|
+
// workspace overrides. This is a dev-only convenience path — production
|
|
381
|
+
// pairing goes through the native messaging flow.
|
|
382
|
+
return join(homedir(), ".vellum", "daemon-token");
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Write a freshly-minted capability token to `~/.vellum/daemon-token` with
|
|
387
|
+
* 0600 permissions. Swallows errors so a failure here never blocks daemon
|
|
388
|
+
* startup — this is a dev-convenience path, not a production auth
|
|
389
|
+
* requirement.
|
|
390
|
+
*/
|
|
391
|
+
export function writeDaemonTokenFallback(guardianId: string): void {
|
|
392
|
+
try {
|
|
393
|
+
const { token } = mintHostBrowserCapability(guardianId);
|
|
394
|
+
const filePath = getDaemonTokenFilePath();
|
|
395
|
+
const dir = dirname(filePath);
|
|
396
|
+
if (!existsSync(dir)) {
|
|
397
|
+
mkdirSync(dir, { recursive: true });
|
|
398
|
+
}
|
|
399
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
400
|
+
writeFileSync(tmpPath, token, { mode: 0o600 });
|
|
401
|
+
renameSync(tmpPath, filePath);
|
|
402
|
+
try {
|
|
403
|
+
chmodSync(filePath, 0o600);
|
|
404
|
+
} catch {
|
|
405
|
+
// best-effort
|
|
406
|
+
}
|
|
407
|
+
log.info({ filePath }, "Dev capability token written to daemon-token file");
|
|
408
|
+
} catch (err) {
|
|
409
|
+
log.warn(
|
|
410
|
+
{ err },
|
|
411
|
+
"Failed to write dev capability token file; manual pairing still available via /v1/browser-extension-pair",
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { addRule } from "../permissions/trust-store.js";
|
|
13
13
|
import type { UserDecision } from "../permissions/types.js";
|
|
14
|
+
import { isPermissionControlsV2Enabled } from "../permissions/v2-consent-policy.js";
|
|
14
15
|
import { getTool } from "../tools/registry.js";
|
|
15
16
|
import { composeApprovalMessage } from "./approval-message-composer.js";
|
|
16
17
|
import type {
|
|
@@ -22,6 +23,7 @@ import type {
|
|
|
22
23
|
import { toApprovalActionOptions } from "./channel-approval-types.js";
|
|
23
24
|
import {
|
|
24
25
|
buildDecisionActions,
|
|
26
|
+
buildOneTimeDecisionActions,
|
|
25
27
|
buildPlainTextFallback,
|
|
26
28
|
} from "./guardian-decision-types.js";
|
|
27
29
|
import * as pendingInteractions from "./pending-interactions.js";
|
|
@@ -92,9 +94,11 @@ function buildPromptFromApprovalInfo(
|
|
|
92
94
|
toolName: info.toolName,
|
|
93
95
|
});
|
|
94
96
|
|
|
95
|
-
const decisionActions =
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const decisionActions = isPermissionControlsV2Enabled()
|
|
98
|
+
? buildOneTimeDecisionActions()
|
|
99
|
+
: buildDecisionActions({
|
|
100
|
+
persistentDecisionsAllowed: info.persistentDecisionsAllowed,
|
|
101
|
+
});
|
|
98
102
|
const actions = toApprovalActionOptions(decisionActions);
|
|
99
103
|
const plainTextFallback = buildPlainTextFallback(promptText, decisionActions);
|
|
100
104
|
|
|
@@ -138,6 +142,10 @@ export function buildApprovalUIMetadata(
|
|
|
138
142
|
* the permission pipeline can activate the appropriate override.
|
|
139
143
|
*/
|
|
140
144
|
function mapApprovalActionToUserDecision(action: ApprovalAction): UserDecision {
|
|
145
|
+
if (isPermissionControlsV2Enabled()) {
|
|
146
|
+
return action === "reject" ? "deny" : "allow";
|
|
147
|
+
}
|
|
148
|
+
|
|
141
149
|
switch (action) {
|
|
142
150
|
case "reject":
|
|
143
151
|
return "deny";
|
|
@@ -182,7 +190,10 @@ export function handleChannelDecision(
|
|
|
182
190
|
: pending[0];
|
|
183
191
|
if (!info) return { applied: false };
|
|
184
192
|
|
|
185
|
-
if (
|
|
193
|
+
if (
|
|
194
|
+
!isPermissionControlsV2Enabled() &&
|
|
195
|
+
decision.action === "approve_always"
|
|
196
|
+
) {
|
|
186
197
|
// Only persist a trust rule when the confirmation explicitly allows persistence
|
|
187
198
|
// AND provides explicit allowlist/scope options. Without explicit options we
|
|
188
199
|
// would create a blanket "**"/"everywhere" rule, which is a security risk.
|
|
@@ -264,7 +275,9 @@ export function buildGuardianApprovalPrompt(
|
|
|
264
275
|
requesterIdentifier,
|
|
265
276
|
});
|
|
266
277
|
|
|
267
|
-
const decisionActions =
|
|
278
|
+
const decisionActions = isPermissionControlsV2Enabled()
|
|
279
|
+
? buildOneTimeDecisionActions()
|
|
280
|
+
: buildDecisionActions({ forGuardianOnBehalf: true });
|
|
268
281
|
const actions = toApprovalActionOptions(decisionActions);
|
|
269
282
|
const plainTextFallback = buildPlainTextFallback(promptText, decisionActions);
|
|
270
283
|
|