@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
|
@@ -18,14 +18,14 @@ export interface TemporalContextOptions {
|
|
|
18
18
|
userTimeZone?: string | null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
21
|
+
const WEEKDAY_LONG = [
|
|
22
|
+
"Sunday",
|
|
23
|
+
"Monday",
|
|
24
|
+
"Tuesday",
|
|
25
|
+
"Wednesday",
|
|
26
|
+
"Thursday",
|
|
27
|
+
"Friday",
|
|
28
|
+
"Saturday",
|
|
29
29
|
] as const;
|
|
30
30
|
const UTC_GMT_OFFSET_TOKEN_RE = /^(?:UTC|GMT)([+-])(\d{1,2})(?::?(\d{2}))?$/i;
|
|
31
31
|
|
|
@@ -295,7 +295,7 @@ function formatLocalDate(date: Date, timeZone: string): string {
|
|
|
295
295
|
* Uses the timezone resolution cascade:
|
|
296
296
|
* explicit override → configured user tz → profile user tz → host fallback.
|
|
297
297
|
*
|
|
298
|
-
* Returns format: `2026-04-02 (
|
|
298
|
+
* Returns format: `2026-04-02 (Thursday) 01:52:33 -05:00 (America/Chicago)`
|
|
299
299
|
*/
|
|
300
300
|
export function formatTurnTimestamp(
|
|
301
301
|
options: TemporalContextOptions = {},
|
|
@@ -322,7 +322,7 @@ export function formatTurnTimestamp(
|
|
|
322
322
|
|
|
323
323
|
const dateStr = formatLocalDate(now, timeZone);
|
|
324
324
|
const todayParts = localDateParts(now, timeZone);
|
|
325
|
-
const dayName =
|
|
325
|
+
const dayName = WEEKDAY_LONG[todayParts.weekday];
|
|
326
326
|
|
|
327
327
|
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
328
328
|
timeZone,
|
|
@@ -5,10 +5,11 @@ import { getWorkspacePromptPath } from "../util/platform.js";
|
|
|
5
5
|
/**
|
|
6
6
|
* The canned assistant response for the wake-up greeting on a fresh workspace.
|
|
7
7
|
* Warm, non-presumptuous greeting that communicates "I'm new," "I improve over
|
|
8
|
-
* time,"
|
|
8
|
+
* time," and invites the user to lead with whatever they want — a task, a
|
|
9
|
+
* question, or getting to know each other.
|
|
9
10
|
*/
|
|
10
11
|
export const CANNED_FIRST_GREETING =
|
|
11
|
-
"Hey
|
|
12
|
+
"Hey — I'm brand new. No name, no memories, no idea who you are yet. I'll get sharper the more we work together. What can I do for you?";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Returns `true` when all of the following are true:
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { v4 as uuid } from "uuid";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
type InterfaceId,
|
|
5
|
-
parseChannelId,
|
|
6
|
-
parseInterfaceId,
|
|
7
|
-
supportsHostProxy,
|
|
8
|
-
} from "../../channels/types.js";
|
|
9
3
|
import { getConfig } from "../../config/loader.js";
|
|
10
4
|
import {
|
|
11
5
|
createCanonicalGuardianRequest,
|
|
@@ -14,28 +8,18 @@ import {
|
|
|
14
8
|
import {
|
|
15
9
|
batchSetDisplayOrders,
|
|
16
10
|
clearAll,
|
|
17
|
-
createConversation,
|
|
18
11
|
getConversation,
|
|
19
12
|
updateConversationTitle,
|
|
20
13
|
} from "../../memory/conversation-crud.js";
|
|
21
14
|
import { resolveConversationId } from "../../memory/conversation-key-store.js";
|
|
22
|
-
import {
|
|
23
|
-
GENERATING_TITLE,
|
|
24
|
-
queueGenerateConversationTitle,
|
|
25
|
-
UNTITLED_FALLBACK,
|
|
26
|
-
} from "../../memory/conversation-title-service.js";
|
|
27
15
|
import * as pendingInteractions from "../../runtime/pending-interactions.js";
|
|
28
16
|
import { redactSecrets } from "../../security/secret-scanner.js";
|
|
29
17
|
import { getSubagentManager } from "../../subagent/index.js";
|
|
30
18
|
import { summarizeToolInput } from "../../tools/tool-input-summary.js";
|
|
31
19
|
import { truncate } from "../../util/truncate.js";
|
|
32
20
|
import type { Conversation } from "../conversation.js";
|
|
33
|
-
import { HostBashProxy } from "../host-bash-proxy.js";
|
|
34
|
-
import { HostCuProxy } from "../host-cu-proxy.js";
|
|
35
|
-
import { HostFileProxy } from "../host-file-proxy.js";
|
|
36
21
|
import type {
|
|
37
22
|
ConfirmationResponse,
|
|
38
|
-
ConversationCreateRequest,
|
|
39
23
|
ConversationRenameRequest,
|
|
40
24
|
ConversationSwitchRequest,
|
|
41
25
|
DeleteQueuedMessage,
|
|
@@ -132,6 +116,12 @@ export function makeEventSender(params: {
|
|
|
132
116
|
conversationId,
|
|
133
117
|
kind: "host_bash",
|
|
134
118
|
});
|
|
119
|
+
} else if (event.type === "host_browser_request") {
|
|
120
|
+
pendingInteractions.register(event.requestId, {
|
|
121
|
+
conversation,
|
|
122
|
+
conversationId,
|
|
123
|
+
kind: "host_browser",
|
|
124
|
+
});
|
|
135
125
|
} else if (event.type === "host_file_request") {
|
|
136
126
|
pendingInteractions.register(event.requestId, {
|
|
137
127
|
conversation,
|
|
@@ -227,130 +217,6 @@ export function clearAllConversations(ctx: HandlerContext): number {
|
|
|
227
217
|
return cleared;
|
|
228
218
|
}
|
|
229
219
|
|
|
230
|
-
export async function handleConversationCreate(
|
|
231
|
-
msg: ConversationCreateRequest,
|
|
232
|
-
ctx: HandlerContext,
|
|
233
|
-
): Promise<void> {
|
|
234
|
-
const conversationType = normalizeConversationType(msg.conversationType);
|
|
235
|
-
const title =
|
|
236
|
-
msg.title ?? (msg.initialMessage ? GENERATING_TITLE : "New Conversation");
|
|
237
|
-
const conversation = createConversation({
|
|
238
|
-
title,
|
|
239
|
-
conversationType,
|
|
240
|
-
});
|
|
241
|
-
const conversationObj = await ctx.getOrCreateConversation(conversation.id, {
|
|
242
|
-
systemPromptOverride: msg.systemPromptOverride,
|
|
243
|
-
maxResponseTokens: msg.maxResponseTokens,
|
|
244
|
-
transport: msg.transport,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// Pre-activate skills before sending conversation_info so they're available
|
|
248
|
-
// for the initial message processing.
|
|
249
|
-
if (msg.preactivatedSkillIds?.length) {
|
|
250
|
-
conversationObj.setPreactivatedSkillIds(msg.preactivatedSkillIds);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
ctx.send({
|
|
254
|
-
type: "conversation_info",
|
|
255
|
-
conversationId: conversation.id,
|
|
256
|
-
title: conversation.title ?? "New Conversation",
|
|
257
|
-
...(msg.correlationId ? { correlationId: msg.correlationId } : {}),
|
|
258
|
-
conversationType: normalizeConversationType(conversation.conversationType),
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
// Auto-send the initial message if provided, kick-starting the skill.
|
|
262
|
-
if (msg.initialMessage) {
|
|
263
|
-
// Queue title generation eagerly — some processMessage paths (guardian
|
|
264
|
-
// replies, unknown slash commands) bypass the agent loop entirely, so
|
|
265
|
-
// we can't rely on the agent loop's early title generation alone.
|
|
266
|
-
// The agent loop also queues title generation, but isReplaceableTitle
|
|
267
|
-
// prevents double-writes since the first to complete sets a real title.
|
|
268
|
-
if (title === GENERATING_TITLE) {
|
|
269
|
-
queueGenerateConversationTitle({
|
|
270
|
-
conversationId: conversation.id,
|
|
271
|
-
context: { origin: "local" },
|
|
272
|
-
userMessage: msg.initialMessage,
|
|
273
|
-
onTitleUpdated: (newTitle) => {
|
|
274
|
-
ctx.send({
|
|
275
|
-
type: "conversation_title_updated",
|
|
276
|
-
conversationId: conversation.id,
|
|
277
|
-
title: newTitle,
|
|
278
|
-
});
|
|
279
|
-
},
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const requestId = uuid();
|
|
284
|
-
const transportChannel =
|
|
285
|
-
parseChannelId(msg.transport?.channelId) ?? "vellum";
|
|
286
|
-
const sendEvent = makeEventSender({
|
|
287
|
-
ctx,
|
|
288
|
-
conversation: conversationObj,
|
|
289
|
-
conversationId: conversation.id,
|
|
290
|
-
sourceChannel: transportChannel,
|
|
291
|
-
});
|
|
292
|
-
conversationObj.setTurnChannelContext({
|
|
293
|
-
userMessageChannel: transportChannel,
|
|
294
|
-
assistantMessageChannel: transportChannel,
|
|
295
|
-
});
|
|
296
|
-
const transportInterface: InterfaceId =
|
|
297
|
-
parseInterfaceId(msg.transport?.interfaceId) ?? "vellum";
|
|
298
|
-
conversationObj.setTurnInterfaceContext({
|
|
299
|
-
userMessageInterface: transportInterface,
|
|
300
|
-
assistantMessageInterface: transportInterface,
|
|
301
|
-
});
|
|
302
|
-
// Only create the host bash proxy for desktop client interfaces that can
|
|
303
|
-
// execute commands on the user's machine. Set before updateClient so
|
|
304
|
-
// updateClient's call to hostBashProxy.updateSender targets the new proxy.
|
|
305
|
-
if (supportsHostProxy(transportInterface)) {
|
|
306
|
-
const proxy = new HostBashProxy(sendEvent, (requestId) => {
|
|
307
|
-
pendingInteractions.resolve(requestId);
|
|
308
|
-
});
|
|
309
|
-
conversationObj.setHostBashProxy(proxy);
|
|
310
|
-
const fileProxy = new HostFileProxy(sendEvent, (requestId) => {
|
|
311
|
-
pendingInteractions.resolve(requestId);
|
|
312
|
-
});
|
|
313
|
-
conversationObj.setHostFileProxy(fileProxy);
|
|
314
|
-
const cuProxy = new HostCuProxy(sendEvent, (requestId) => {
|
|
315
|
-
pendingInteractions.resolve(requestId);
|
|
316
|
-
});
|
|
317
|
-
conversationObj.setHostCuProxy(cuProxy);
|
|
318
|
-
conversationObj.addPreactivatedSkillId("computer-use");
|
|
319
|
-
}
|
|
320
|
-
conversationObj.updateClient(sendEvent, false);
|
|
321
|
-
conversationObj
|
|
322
|
-
.processMessage(msg.initialMessage, [], sendEvent, requestId)
|
|
323
|
-
.catch((err) => {
|
|
324
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
325
|
-
log.error(
|
|
326
|
-
{ err, conversationId: conversation.id },
|
|
327
|
-
"Error processing initial message",
|
|
328
|
-
);
|
|
329
|
-
ctx.send({
|
|
330
|
-
type: "error",
|
|
331
|
-
message: `Failed to process initial message: ${message}`,
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
// Replace stuck loading placeholder with a stable fallback title
|
|
335
|
-
// if title generation hasn't already completed or been renamed.
|
|
336
|
-
try {
|
|
337
|
-
const current = getConversation(conversation.id);
|
|
338
|
-
if (current && current.title === GENERATING_TITLE) {
|
|
339
|
-
const fallback = UNTITLED_FALLBACK;
|
|
340
|
-
updateConversationTitle(conversation.id, fallback);
|
|
341
|
-
ctx.send({
|
|
342
|
-
type: "conversation_title_updated",
|
|
343
|
-
conversationId: conversation.id,
|
|
344
|
-
title: fallback,
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
} catch {
|
|
348
|
-
// Best-effort fallback
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
220
|
/**
|
|
355
221
|
* Switch to an existing conversation. Returns conversation info on success,
|
|
356
222
|
* or throws/returns an error result when the conversation is not found.
|
|
@@ -362,6 +228,7 @@ export async function switchConversation(
|
|
|
362
228
|
conversationId: string;
|
|
363
229
|
title: string;
|
|
364
230
|
conversationType: ReturnType<typeof normalizeConversationType>;
|
|
231
|
+
hostAccess: boolean;
|
|
365
232
|
} | null> {
|
|
366
233
|
const conversation = getConversation(conversationId);
|
|
367
234
|
if (!conversation) {
|
|
@@ -384,6 +251,7 @@ export async function switchConversation(
|
|
|
384
251
|
conversationId: conversation.id,
|
|
385
252
|
title: conversation.title ?? "Untitled",
|
|
386
253
|
conversationType: normalizeConversationType(conversation.conversationType),
|
|
254
|
+
hostAccess: conversation.hostAccess === 1,
|
|
387
255
|
};
|
|
388
256
|
}
|
|
389
257
|
|
|
@@ -405,6 +273,7 @@ export async function handleConversationSwitch(
|
|
|
405
273
|
conversationId: result.conversationId,
|
|
406
274
|
title: result.title,
|
|
407
275
|
conversationType: result.conversationType,
|
|
276
|
+
hostAccess: result.hostAccess,
|
|
408
277
|
});
|
|
409
278
|
}
|
|
410
279
|
|
|
@@ -110,6 +110,11 @@ export interface ConversationCreateOptions {
|
|
|
110
110
|
transport?: ConversationTransportMetadata;
|
|
111
111
|
assistantId?: string;
|
|
112
112
|
trustContext?: TrustContext;
|
|
113
|
+
/**
|
|
114
|
+
* Active task-run scope for this turn. Cleared when omitted so background
|
|
115
|
+
* task permissions do not leak into later turns on a reused conversation.
|
|
116
|
+
*/
|
|
117
|
+
taskRunId?: string;
|
|
113
118
|
/** Normalized auth context for the conversation. */
|
|
114
119
|
authContext?: AuthContext;
|
|
115
120
|
/** Whether this turn can block on interactive approval prompts. */
|
|
@@ -344,6 +349,59 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
|
|
|
344
349
|
}
|
|
345
350
|
continue;
|
|
346
351
|
}
|
|
352
|
+
if (block.type === "server_tool_use") {
|
|
353
|
+
finalizeSegment();
|
|
354
|
+
const name = typeof block.name === "string" ? block.name : "unknown";
|
|
355
|
+
const input = isRecord(block.input)
|
|
356
|
+
? (block.input as Record<string, unknown>)
|
|
357
|
+
: {};
|
|
358
|
+
const id = typeof block.id === "string" ? block.id : "";
|
|
359
|
+
const entry: HistoryToolCall = { name, input };
|
|
360
|
+
toolCalls.push(entry);
|
|
361
|
+
if (id) pendingToolUses.set(id, entry);
|
|
362
|
+
contentOrder.push(`tool:${toolCalls.length - 1}`);
|
|
363
|
+
if (!seenToolUse) {
|
|
364
|
+
seenToolUse = true;
|
|
365
|
+
if (!seenText) toolCallsBeforeText = true;
|
|
366
|
+
}
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (block.type === "web_search_tool_result") {
|
|
370
|
+
const toolUseId =
|
|
371
|
+
typeof block.tool_use_id === "string" ? block.tool_use_id : "";
|
|
372
|
+
const isError =
|
|
373
|
+
isRecord(block.content) &&
|
|
374
|
+
(block.content as { type?: string }).type ===
|
|
375
|
+
"web_search_tool_result_error";
|
|
376
|
+
|
|
377
|
+
// Format search results into readable text.
|
|
378
|
+
let resultContent = "";
|
|
379
|
+
if (Array.isArray(block.content)) {
|
|
380
|
+
resultContent = (block.content as unknown[])
|
|
381
|
+
.filter(
|
|
382
|
+
(r): r is { type: string; title: string; url: string } =>
|
|
383
|
+
typeof r === "object" &&
|
|
384
|
+
r != null &&
|
|
385
|
+
(r as { type?: string }).type === "web_search_result",
|
|
386
|
+
)
|
|
387
|
+
.map((r) => `${r.title}\n${r.url}`)
|
|
388
|
+
.join("\n\n");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const matched = toolUseId ? pendingToolUses.get(toolUseId) : null;
|
|
392
|
+
if (matched) {
|
|
393
|
+
matched.result = resultContent;
|
|
394
|
+
matched.isError = isError;
|
|
395
|
+
} else {
|
|
396
|
+
toolCalls.push({
|
|
397
|
+
name: "web_search",
|
|
398
|
+
input: {},
|
|
399
|
+
result: resultContent,
|
|
400
|
+
isError,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
347
405
|
if (block.type === "tool_result") {
|
|
348
406
|
const toolUseId =
|
|
349
407
|
typeof block.tool_use_id === "string" ? block.tool_use_id : "";
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
statSync,
|
|
10
10
|
} from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
|
-
import { join, relative } from "node:path";
|
|
12
|
+
import { basename, join, relative, sep } from "node:path";
|
|
13
13
|
|
|
14
14
|
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
15
15
|
import {
|
|
@@ -31,9 +31,21 @@ import {
|
|
|
31
31
|
getConfiguredProvider,
|
|
32
32
|
userMessage,
|
|
33
33
|
} from "../../providers/provider-send-message.js";
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
isTextMimeType as isTextMime,
|
|
36
|
+
MAX_INLINE_TEXT_SIZE,
|
|
37
|
+
} from "../../runtime/routes/workspace-utils.js";
|
|
35
38
|
import { getCatalog } from "../../skills/catalog-cache.js";
|
|
36
39
|
import {
|
|
40
|
+
hasHiddenOrSkippedSegment,
|
|
41
|
+
readCatalogSkillFileContent,
|
|
42
|
+
readCatalogSkillFiles,
|
|
43
|
+
sanitizeRelativePath,
|
|
44
|
+
type SkillFileEntry,
|
|
45
|
+
SKIP_DIRS,
|
|
46
|
+
} from "../../skills/catalog-files.js";
|
|
47
|
+
import {
|
|
48
|
+
type CatalogSkill,
|
|
37
49
|
installSkillLocally,
|
|
38
50
|
upsertSkillsIndex,
|
|
39
51
|
} from "../../skills/catalog-install.js";
|
|
@@ -64,6 +76,7 @@ import {
|
|
|
64
76
|
import { getWorkspaceSkillsDir } from "../../util/platform.js";
|
|
65
77
|
import type {
|
|
66
78
|
SkillDetailResponse,
|
|
79
|
+
SkillFileContentResponse,
|
|
67
80
|
SlimSkillResponse,
|
|
68
81
|
} from "../message-types/skills.js";
|
|
69
82
|
import {
|
|
@@ -73,10 +86,6 @@ import {
|
|
|
73
86
|
log,
|
|
74
87
|
} from "./shared.js";
|
|
75
88
|
|
|
76
|
-
// ─── MIME detection helpers ───────────────────────────────────────────────────
|
|
77
|
-
|
|
78
|
-
const MAX_INLINE_SIZE = 2 * 1024 * 1024; // 2 MB
|
|
79
|
-
|
|
80
89
|
// ─── Shared context for standalone functions ─────────────────────────────────
|
|
81
90
|
|
|
82
91
|
/**
|
|
@@ -364,7 +373,7 @@ export async function listSkillsWithCatalog(
|
|
|
364
373
|
const installed = listSkills(ctx);
|
|
365
374
|
const installedIds = new Set(installed.map((s) => s.id));
|
|
366
375
|
|
|
367
|
-
let catalogSkills:
|
|
376
|
+
let catalogSkills: CatalogSkill[];
|
|
368
377
|
try {
|
|
369
378
|
catalogSkills = await getCatalog();
|
|
370
379
|
} catch {
|
|
@@ -376,15 +385,7 @@ export async function listSkillsWithCatalog(
|
|
|
376
385
|
// Create SlimSkillResponses for catalog skills not already installed.
|
|
377
386
|
const available: SlimSkillResponse[] = catalogSkills
|
|
378
387
|
.filter((cs) => !installedIds.has(cs.id))
|
|
379
|
-
.map((cs) => (
|
|
380
|
-
id: cs.id,
|
|
381
|
-
name: cs.metadata?.vellum?.["display-name"] ?? cs.name,
|
|
382
|
-
description: cs.description,
|
|
383
|
-
emoji: cs.emoji,
|
|
384
|
-
kind: "catalog" as const,
|
|
385
|
-
origin: "vellum" as const,
|
|
386
|
-
status: "available" as const,
|
|
387
|
-
}));
|
|
388
|
+
.map((cs) => catalogSkillToSlim(cs));
|
|
388
389
|
|
|
389
390
|
const merged = [...installed, ...available];
|
|
390
391
|
|
|
@@ -494,16 +495,12 @@ export async function getSkill(
|
|
|
494
495
|
|
|
495
496
|
// ─── Skill file listing ──────────────────────────────────────────────────────
|
|
496
497
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
content: string | null; // inline text if ≤ 2 MB and text MIME, else null
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const SKIP_DIRS = new Set(["node_modules", "__pycache__", ".git"]);
|
|
498
|
+
// `SkillFileEntry` lives in `../../skills/catalog-files.ts` to keep a single
|
|
499
|
+
// source of truth for the shape and avoid a circular import (catalog-files
|
|
500
|
+
// depends on `catalog-cache.ts`, which would otherwise be reachable via this
|
|
501
|
+
// handler module). Re-exported here so handlers can import it alongside
|
|
502
|
+
// the other skill handler exports.
|
|
503
|
+
export type { SkillFileEntry } from "../../skills/catalog-files.js";
|
|
507
504
|
|
|
508
505
|
/**
|
|
509
506
|
* Returns true if `filePath` is a symlink whose resolved real path escapes
|
|
@@ -550,7 +547,7 @@ function readDirRecursive(dir: string, rootDir: string): SkillFileEntry[] {
|
|
|
550
547
|
const mimeType = Bun.file(fullPath).type;
|
|
551
548
|
const isText = isTextMime(mimeType, dirent.name);
|
|
552
549
|
let content: string | null = null;
|
|
553
|
-
if (isText && stat.size <=
|
|
550
|
+
if (isText && stat.size <= MAX_INLINE_TEXT_SIZE) {
|
|
554
551
|
content = readFileSync(fullPath, "utf-8");
|
|
555
552
|
}
|
|
556
553
|
entries.push({
|
|
@@ -568,26 +565,224 @@ function readDirRecursive(dir: string, rootDir: string): SkillFileEntry[] {
|
|
|
568
565
|
return entries;
|
|
569
566
|
}
|
|
570
567
|
|
|
571
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Map a `CatalogSkill` (from the Vellum platform API) to a `SlimSkillResponse`
|
|
570
|
+
* shaped for the "available catalog skill" case. Shared between
|
|
571
|
+
* `listSkillsWithCatalog` (merging catalog entries into the installed list)
|
|
572
|
+
* and `getSkillFiles` (catalog fallback for preview listings). Keeping the
|
|
573
|
+
* mapping in one place avoids divergence between the list and detail paths.
|
|
574
|
+
*/
|
|
575
|
+
function catalogSkillToSlim(cs: CatalogSkill): SlimSkillResponse {
|
|
576
|
+
return {
|
|
577
|
+
id: cs.id,
|
|
578
|
+
name: cs.metadata?.vellum?.["display-name"] ?? cs.name,
|
|
579
|
+
description: cs.description,
|
|
580
|
+
emoji: cs.emoji,
|
|
581
|
+
kind: "catalog",
|
|
582
|
+
origin: "vellum",
|
|
583
|
+
status: "available",
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Read a single file's content from an installed or catalog skill.
|
|
589
|
+
*
|
|
590
|
+
* Installed-skill path (eager): reads the file directly from the skill's
|
|
591
|
+
* on-disk directory. Applies lexical containment, symlink rejection, and
|
|
592
|
+
* realpath containment checks for defense in depth.
|
|
593
|
+
*
|
|
594
|
+
* Catalog fallback: when the skill id is not backed by a local directory
|
|
595
|
+
* (e.g. an uninstalled Vellum catalog skill), delegates to
|
|
596
|
+
* `readCatalogSkillFileContent`, which handles both the dev-mode repo
|
|
597
|
+
* checkout path and the platform preview API path internally.
|
|
598
|
+
*/
|
|
599
|
+
export async function getSkillFileContent(
|
|
572
600
|
skillId: string,
|
|
601
|
+
relativePath: string,
|
|
573
602
|
_ctx: SkillOperationContext,
|
|
574
|
-
):
|
|
603
|
+
): Promise<SkillFileContentResponse | { error: string; status: number }> {
|
|
604
|
+
const sanitized = sanitizeRelativePath(relativePath);
|
|
605
|
+
if (!sanitized) {
|
|
606
|
+
return { error: "Invalid path", status: 400 };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Reject any sanitized path that references a hidden segment (dotfiles
|
|
610
|
+
// like `.env`, dot-dirs like `.git`) or a SKIP_DIRS segment (e.g.
|
|
611
|
+
// `node_modules`, `__pycache__`). Both file-listing endpoints (installed
|
|
612
|
+
// and catalog) intentionally omit these entries, so allowing the content
|
|
613
|
+
// endpoint to read them would create a data-exposure path and break
|
|
614
|
+
// parity with the visible file list. This check runs BEFORE both the
|
|
615
|
+
// installed-skill disk read and the catalog fallback so the rejection
|
|
616
|
+
// is uniform regardless of source.
|
|
617
|
+
if (hasHiddenOrSkippedSegment(sanitized)) {
|
|
618
|
+
return { error: "Invalid path", status: 400 };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const found = findSkillById(skillId);
|
|
622
|
+
if (found) {
|
|
623
|
+
if (!existsSync(found.summary.directoryPath)) {
|
|
624
|
+
// Resolver lists the skill as installed but the directory is missing
|
|
625
|
+
// on disk (corrupted install, mid-delete race, external unmount, etc.).
|
|
626
|
+
// Return a distinct 404 instead of falling through to the catalog path
|
|
627
|
+
// so the content response stays consistent with `listSkillsWithCatalog`
|
|
628
|
+
// and `getSkillFiles`, which classify the same id as `kind: "installed"`.
|
|
629
|
+
return {
|
|
630
|
+
error: `Skill directory missing for "${skillId}"`,
|
|
631
|
+
status: 404,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
const dir = found.summary.directoryPath;
|
|
635
|
+
const abs = join(dir, sanitized);
|
|
636
|
+
|
|
637
|
+
// Lexical containment: the resolved absolute path must stay inside the
|
|
638
|
+
// skill directory even after `join` normalization. Cheap short-circuit
|
|
639
|
+
// before any fs calls.
|
|
640
|
+
if (!(abs === dir || abs.startsWith(dir + sep))) {
|
|
641
|
+
return { error: "Invalid path", status: 400 };
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Defense-in-depth symlink rejection: refuse to follow a symlinked file
|
|
645
|
+
// inside the skill dir that could point outside the root. Also catches
|
|
646
|
+
// symlinked parent directories via a realpath containment check.
|
|
647
|
+
let lstat;
|
|
648
|
+
try {
|
|
649
|
+
lstat = lstatSync(abs);
|
|
650
|
+
} catch {
|
|
651
|
+
return { error: "File not found", status: 404 };
|
|
652
|
+
}
|
|
653
|
+
if (lstat.isSymbolicLink()) {
|
|
654
|
+
return { error: "File not found", status: 404 };
|
|
655
|
+
}
|
|
656
|
+
if (!lstat.isFile()) {
|
|
657
|
+
return { error: "File not found", status: 404 };
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
let realAbs: string;
|
|
661
|
+
let realDir: string;
|
|
662
|
+
try {
|
|
663
|
+
realAbs = realpathSync(abs);
|
|
664
|
+
realDir = realpathSync(dir);
|
|
665
|
+
} catch {
|
|
666
|
+
return { error: "File not found", status: 404 };
|
|
667
|
+
}
|
|
668
|
+
if (!(realAbs === realDir || realAbs.startsWith(realDir + sep))) {
|
|
669
|
+
return { error: "File not found", status: 404 };
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
let stat;
|
|
673
|
+
try {
|
|
674
|
+
stat = statSync(abs);
|
|
675
|
+
} catch {
|
|
676
|
+
return { error: "File not found", status: 404 };
|
|
677
|
+
}
|
|
678
|
+
if (!stat.isFile()) {
|
|
679
|
+
return { error: "File not found", status: 404 };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const name = basename(sanitized);
|
|
683
|
+
const mimeType = Bun.file(abs).type;
|
|
684
|
+
const isText = isTextMime(mimeType, name);
|
|
685
|
+
const isBinary = !isText;
|
|
686
|
+
let content: string | null = null;
|
|
687
|
+
if (isText && stat.size <= MAX_INLINE_TEXT_SIZE) {
|
|
688
|
+
try {
|
|
689
|
+
content = readFileSync(abs, "utf-8");
|
|
690
|
+
} catch {
|
|
691
|
+
content = null;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
path: sanitized,
|
|
696
|
+
name,
|
|
697
|
+
size: stat.size,
|
|
698
|
+
mimeType,
|
|
699
|
+
isBinary,
|
|
700
|
+
content,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Catalog fallback: skill is not installed locally. Try the catalog
|
|
705
|
+
// preview helper, which handles both dev-mode repo checkouts and the
|
|
706
|
+
// platform preview API.
|
|
707
|
+
let catalog: Awaited<ReturnType<typeof getCatalog>> = [];
|
|
708
|
+
try {
|
|
709
|
+
catalog = await getCatalog();
|
|
710
|
+
} catch {
|
|
711
|
+
catalog = [];
|
|
712
|
+
}
|
|
713
|
+
const inCatalog = catalog.some((s) => s.id === skillId);
|
|
714
|
+
if (!inCatalog) {
|
|
715
|
+
return { error: "Skill not found", status: 404 };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const result = await readCatalogSkillFileContent(skillId, sanitized);
|
|
719
|
+
if (!result) {
|
|
720
|
+
return { error: "File not found", status: 404 };
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
path: result.path,
|
|
724
|
+
name: result.name,
|
|
725
|
+
size: result.size,
|
|
726
|
+
mimeType: result.mimeType,
|
|
727
|
+
isBinary: result.isBinary,
|
|
728
|
+
content: result.content,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export async function getSkillFiles(
|
|
733
|
+
skillId: string,
|
|
734
|
+
_ctx: SkillOperationContext,
|
|
735
|
+
): Promise<
|
|
575
736
|
| { skill: SlimSkillResponse; files: SkillFileEntry[] }
|
|
576
|
-
| { error: string; status: number }
|
|
737
|
+
| { error: string; status: number }
|
|
738
|
+
> {
|
|
739
|
+
// Preferred path: the skill is resolved locally (bundled, managed,
|
|
740
|
+
// workspace, or extra) AND its directory exists on disk. Read files
|
|
741
|
+
// eagerly with inline content.
|
|
577
742
|
const found = findSkillById(skillId);
|
|
578
|
-
if (
|
|
743
|
+
if (found) {
|
|
744
|
+
if (existsSync(found.summary.directoryPath)) {
|
|
745
|
+
const dirPath = found.summary.directoryPath;
|
|
746
|
+
const files = readDirRecursive(dirPath, dirPath);
|
|
747
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
748
|
+
return { skill: found.item, files };
|
|
749
|
+
}
|
|
750
|
+
// Resolver lists the skill as installed but the directory is missing
|
|
751
|
+
// on disk (corrupted install, mid-delete race, external unmount, etc.).
|
|
752
|
+
// Return a distinct 404 instead of falling through to the catalog path
|
|
753
|
+
// so the detail response stays consistent with `listSkillsWithCatalog`,
|
|
754
|
+
// which classifies the same id as `kind: "installed"`.
|
|
755
|
+
return {
|
|
756
|
+
error: `Skill directory missing for "${skillId}"`,
|
|
757
|
+
status: 404,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Fallback: skill is not installed. Try the Vellum catalog — this covers
|
|
762
|
+
// previewing files for an uninstalled catalog skill without touching the
|
|
763
|
+
// install flow.
|
|
764
|
+
let catalog: CatalogSkill[];
|
|
765
|
+
try {
|
|
766
|
+
catalog = await getCatalog();
|
|
767
|
+
} catch {
|
|
768
|
+
return { error: `Skill "${skillId}" not found`, status: 404 };
|
|
769
|
+
}
|
|
770
|
+
const cs = catalog.find((c) => c.id === skillId);
|
|
771
|
+
if (!cs) {
|
|
579
772
|
return { error: `Skill "${skillId}" not found`, status: 404 };
|
|
580
773
|
}
|
|
581
774
|
|
|
582
|
-
const
|
|
583
|
-
if (
|
|
584
|
-
return {
|
|
775
|
+
const files = await readCatalogSkillFiles(skillId);
|
|
776
|
+
if (files === null) {
|
|
777
|
+
return {
|
|
778
|
+
error: `Skill files unavailable for "${skillId}"`,
|
|
779
|
+
status: 404,
|
|
780
|
+
};
|
|
585
781
|
}
|
|
586
782
|
|
|
587
|
-
const
|
|
783
|
+
const skill = catalogSkillToSlim(cs);
|
|
588
784
|
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
589
|
-
|
|
590
|
-
return { skill: found.item, files };
|
|
785
|
+
return { skill, files };
|
|
591
786
|
}
|
|
592
787
|
|
|
593
788
|
export function enableSkill(
|