@vellumai/assistant 0.6.1 → 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/docker-entrypoint.sh +12 -2
- package/docs/architecture/memory.md +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- 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__/assistant-event-hub.test.ts +30 -0
- 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__/checker.test.ts +104 -170
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- 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 +21 -6
- 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 +169 -0
- package/src/__tests__/conversation-attachments.test.ts +80 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +155 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -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 -3
- 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__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- package/src/__tests__/integration-status.test.ts +6 -7
- package/src/__tests__/list-messages-tool-merge.test.ts +37 -12
- package/src/__tests__/log-export-workspace.test.ts +190 -0
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- 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__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -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 +74 -55
- 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__/pkb-autoinject.test.ts +96 -0
- 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 -3
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- 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-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +11 -5
- package/src/__tests__/test-preload.ts +14 -0
- package/src/__tests__/tool-approval-handler.test.ts +73 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +0 -1
- 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 +62 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +109 -0
- package/src/__tests__/v2-consent-policy.test.ts +103 -0
- package/src/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/acp/client-handler.ts +30 -4
- package/src/agent/loop.ts +12 -35
- 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 +55 -0
- package/src/cli/__tests__/run-assistant-command.ts +34 -7
- package/src/cli/__tests__/unknown-command.test.ts +33 -0
- package/src/cli/commands/browser-relay.ts +339 -409
- package/src/cli/commands/credentials.ts +3 -3
- package/src/cli/commands/default-action.ts +68 -1
- 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 +68 -41
- 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 +16 -2
- 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/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- 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 +10 -3
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +33 -173
- 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 +12 -7
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- package/src/config/bundled-skills/outlook/SKILL.md +7 -0
- package/src/config/bundled-skills/settings/TOOLS.json +1 -1
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +8 -3
- 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 +46 -7
- 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 +16 -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 -16
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +186 -0
- package/src/daemon/app-source-watcher.ts +35 -0
- package/src/daemon/config-watcher.ts +6 -2
- package/src/daemon/context-overflow-approval.ts +5 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +17 -2
- package/src/daemon/conversation-agent-loop.ts +74 -19
- package/src/daemon/conversation-attachments.ts +40 -1
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +66 -3
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +159 -20
- package/src/daemon/conversation-surfaces.ts +78 -12
- package/src/daemon/conversation-tool-setup.ts +74 -11
- package/src/daemon/conversation-workspace.ts +12 -0
- package/src/daemon/conversation.ts +227 -11
- package/src/daemon/date-context.ts +10 -10
- package/src/daemon/first-greeting.ts +3 -2
- package/src/daemon/handlers/conversations.ts +9 -139
- package/src/daemon/handlers/shared.ts +65 -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 +86 -12
- package/src/daemon/message-protocol.ts +7 -0
- package/src/daemon/message-types/conversations.ts +59 -13
- package/src/daemon/message-types/host-browser.ts +100 -0
- package/src/daemon/message-types/messages.ts +5 -6
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/settings.ts +12 -0
- package/src/daemon/message-types/skills.ts +10 -0
- package/src/daemon/message-types/subagents.ts +2 -0
- package/src/daemon/server.ts +112 -35
- package/src/daemon/tool-side-effects.ts +6 -0
- package/src/daemon/transport-hints.ts +14 -0
- package/src/inbound/platform-callback-registration.ts +18 -17
- package/src/index.ts +1 -1
- package/src/mcp/client.ts +59 -24
- package/src/memory/app-store.ts +31 -1
- package/src/memory/conversation-crud.ts +38 -10
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +65 -5
- 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 +177 -18
- package/src/memory/graph/capability-seed.ts +3 -5
- 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/group-crud.ts +25 -9
- 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/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- 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 +5 -5
- 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 +127 -87
- package/src/oauth/token-persistence.ts +1 -1
- package/src/permissions/checker.ts +3 -3
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode.ts +4 -11
- package/src/permissions/prompter.ts +13 -3
- package/src/permissions/v2-consent-policy.ts +87 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +18 -21
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +3 -65
- package/src/prompts/templates/BOOTSTRAP.md +59 -96
- package/src/prompts/templates/SOUL.md +11 -11
- 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 +24 -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/auth/token-service.ts +8 -0
- 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 +18 -5
- package/src/runtime/routes/conversation-management-routes.ts +108 -0
- package/src/runtime/routes/conversation-routes.ts +308 -28
- package/src/runtime/routes/conversation-starter-routes.ts +78 -16
- package/src/runtime/routes/group-routes.ts +22 -8
- 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/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +60 -25
- 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/skills/inline-command-runner.ts +12 -14
- 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 -100
- package/src/tools/registry.ts +0 -2
- package/src/tools/secret-detection-handler.ts +34 -1
- package/src/tools/shared/filesystem/image-read.ts +61 -40
- package/src/tools/skills/sandbox-runner.ts +3 -6
- 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/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +24 -21
- package/src/tools/tool-approval-handler.ts +48 -2
- package/src/tools/types.ts +2 -3
- package/src/util/platform.ts +14 -19
- package/src/watcher/provider-types.ts +1 -1
- package/src/workspace/migrations/029-seed-pkb.ts +1 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/workspace/top-level-renderer.ts +19 -1
- package/src/__tests__/chrome-cdp.test.ts +0 -419
- package/src/__tests__/permission-mode-sse.test.ts +0 -418
- package/src/__tests__/permission-mode-store.test.ts +0 -277
- package/src/browser-extension-relay/protocol.ts +0 -63
- package/src/browser-extension-relay/server.ts +0 -203
- package/src/config/schemas/sandbox.ts +0 -14
- package/src/permissions/permission-mode-store.ts +0 -180
- package/src/tools/browser/chrome-cdp.ts +0 -239
- package/src/tools/system/set-permission-mode.ts +0 -103
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler-level tests for `getSkillFileContent`.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Installed skill, valid path → returns file content (text + binary).
|
|
6
|
+
* - Installed skill, traversal path → 400 "Invalid path".
|
|
7
|
+
* - Installed skill, missing file → 404 "File not found".
|
|
8
|
+
* - Installed skill with missing on-disk directory → 404 "Skill directory
|
|
9
|
+
* missing" without consulting the catalog fallback.
|
|
10
|
+
* - Uninstalled catalog skill → delegates to `readCatalogSkillFileContent`
|
|
11
|
+
* and returns its payload; null result → 404.
|
|
12
|
+
* - Skill not found anywhere → 404.
|
|
13
|
+
* - Hidden / SKIP_DIRS path segments → rejected with 400 before touching
|
|
14
|
+
* either the installed-skill disk read or the catalog fallback.
|
|
15
|
+
*
|
|
16
|
+
* The test exercises the daemon handler directly — route wiring is a thin
|
|
17
|
+
* pass-through to this function. The catalog-files module is mocked so the
|
|
18
|
+
* handler's wiring is exercised in isolation; the helper's own dev-mode /
|
|
19
|
+
* platform-mode behavior is covered in `catalog-files.test.ts`.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { tmpdir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
26
|
+
|
|
27
|
+
import type { SkillSummary } from "../config/skills.js";
|
|
28
|
+
import type { SkillFileEntry } from "../skills/catalog-files.js";
|
|
29
|
+
import type { CatalogSkill } from "../skills/catalog-install.js";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Mocks — must be declared before importing the module under test
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
// Suppress logger output
|
|
36
|
+
const noopLogger = {
|
|
37
|
+
info: () => {},
|
|
38
|
+
warn: () => {},
|
|
39
|
+
error: () => {},
|
|
40
|
+
debug: () => {},
|
|
41
|
+
trace: () => {},
|
|
42
|
+
fatal: () => {},
|
|
43
|
+
child: () => noopLogger,
|
|
44
|
+
};
|
|
45
|
+
mock.module("../util/logger.js", () => ({
|
|
46
|
+
getLogger: () => noopLogger,
|
|
47
|
+
getCliLogger: () => noopLogger,
|
|
48
|
+
truncateForLog: (v: string) => v,
|
|
49
|
+
initLogger: () => {},
|
|
50
|
+
pruneOldLogFiles: () => 0,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
// ── Mutable mock state ──────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
let mockResolvedSkills: Array<{
|
|
56
|
+
summary: SkillSummary;
|
|
57
|
+
state: "enabled" | "disabled";
|
|
58
|
+
}> = [];
|
|
59
|
+
let mockCatalog: CatalogSkill[] = [];
|
|
60
|
+
// Ordered log of `readCatalogSkillFileContent` invocations so individual
|
|
61
|
+
// tests can assert that the catalog-fallback helper was (or was not)
|
|
62
|
+
// consulted. Mirrors the `catalogFilesCalls` array pattern used in
|
|
63
|
+
// `skills-files-catalog-fallback.test.ts` for `readCatalogSkillFiles`.
|
|
64
|
+
const catalogFileContentCalls: Array<{ skillId: string; path: string }> = [];
|
|
65
|
+
// Per-test override for the catalog fallback helper. When set, the
|
|
66
|
+
// `catalog-files.js` mock's `readCatalogSkillFileContent` delegates to
|
|
67
|
+
// this function, letting tests return canned payloads without touching
|
|
68
|
+
// disk or the network.
|
|
69
|
+
let mockCatalogFileContentResponder:
|
|
70
|
+
| ((skillId: string, path: string) => Promise<SkillFileEntry | null>)
|
|
71
|
+
| null = null;
|
|
72
|
+
|
|
73
|
+
mock.module("../config/skills.js", () => ({
|
|
74
|
+
loadSkillCatalog: () => mockResolvedSkills.map((r) => r.summary),
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
mock.module("../config/loader.js", () => ({
|
|
78
|
+
getConfig: () => ({}),
|
|
79
|
+
invalidateConfigCache: () => {},
|
|
80
|
+
loadRawConfig: () => ({}),
|
|
81
|
+
saveRawConfig: () => {},
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
mock.module("../config/skill-state.js", () => ({
|
|
85
|
+
resolveSkillStates: () => mockResolvedSkills,
|
|
86
|
+
skillFlagKey: () => null,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
mock.module("../config/assistant-feature-flags.js", () => ({
|
|
90
|
+
isAssistantFeatureFlagEnabled: () => true,
|
|
91
|
+
}));
|
|
92
|
+
|
|
93
|
+
mock.module("../skills/catalog-cache.js", () => ({
|
|
94
|
+
getCatalog: async () => mockCatalog,
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
// The `catalog-files.js` mock replaces `readCatalogSkillFileContent` with
|
|
98
|
+
// a spy wrapper that logs invocations and delegates to a per-test
|
|
99
|
+
// responder. Other exports (`sanitizeRelativePath`,
|
|
100
|
+
// `hasHiddenOrSkippedSegment`, `SKIP_DIRS`, `readCatalogSkillFiles`) are
|
|
101
|
+
// re-implemented inline from the production module so the handler's
|
|
102
|
+
// path-validation code still runs against equivalent logic without
|
|
103
|
+
// recursing back through the mocked module.
|
|
104
|
+
//
|
|
105
|
+
// The spy gives the new "installed skill directory missing" regression
|
|
106
|
+
// test a way to assert that the catalog fallback helper is NOT consulted
|
|
107
|
+
// when `findSkillById` resolves a ghost install — mirroring the
|
|
108
|
+
// `catalogFilesCalls.length === 0` assertion in
|
|
109
|
+
// `skills-files-catalog-fallback.test.ts`.
|
|
110
|
+
const INLINE_SKIP_DIRS = new Set(["node_modules", "__pycache__", ".git"]);
|
|
111
|
+
|
|
112
|
+
function inlineSanitizeRelativePath(rawPath: string): string | null {
|
|
113
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) return null;
|
|
114
|
+
if (rawPath.includes("\0")) return null;
|
|
115
|
+
if (rawPath.startsWith("/")) return null;
|
|
116
|
+
if (/^[a-zA-Z]:[/\\]/.test(rawPath)) return null;
|
|
117
|
+
let candidate = rawPath.replace(/\\/g, "/");
|
|
118
|
+
while (candidate.startsWith("./")) {
|
|
119
|
+
candidate = candidate.slice(2);
|
|
120
|
+
}
|
|
121
|
+
if (candidate.length === 0) return null;
|
|
122
|
+
// posix.normalize without the node import: collapse segments manually.
|
|
123
|
+
const segments: string[] = [];
|
|
124
|
+
for (const seg of candidate.split("/")) {
|
|
125
|
+
if (seg === "" || seg === ".") continue;
|
|
126
|
+
if (seg === "..") {
|
|
127
|
+
if (segments.length === 0) return null;
|
|
128
|
+
segments.pop();
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
segments.push(seg);
|
|
132
|
+
}
|
|
133
|
+
if (segments.length === 0) return null;
|
|
134
|
+
const normalized = segments.join("/");
|
|
135
|
+
if (normalized.startsWith("/")) return null;
|
|
136
|
+
if (/^[a-zA-Z]:[/\\]/.test(normalized)) return null;
|
|
137
|
+
return normalized;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function inlineHasHiddenOrSkippedSegment(sanitized: string): boolean {
|
|
141
|
+
for (const segment of sanitized.split("/")) {
|
|
142
|
+
if (segment.length === 0) continue;
|
|
143
|
+
if (segment.startsWith(".")) return true;
|
|
144
|
+
if (INLINE_SKIP_DIRS.has(segment)) return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
mock.module("../skills/catalog-files.js", () => ({
|
|
150
|
+
SKIP_DIRS: INLINE_SKIP_DIRS,
|
|
151
|
+
sanitizeRelativePath: inlineSanitizeRelativePath,
|
|
152
|
+
hasHiddenOrSkippedSegment: inlineHasHiddenOrSkippedSegment,
|
|
153
|
+
readCatalogSkillFiles: async () => null,
|
|
154
|
+
readCatalogSkillFileContent: async (skillId: string, path: string) => {
|
|
155
|
+
catalogFileContentCalls.push({ skillId, path });
|
|
156
|
+
if (mockCatalogFileContentResponder) {
|
|
157
|
+
return mockCatalogFileContentResponder(skillId, path);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
},
|
|
161
|
+
}));
|
|
162
|
+
|
|
163
|
+
mock.module("../skills/catalog-install.js", () => ({
|
|
164
|
+
installSkillLocally: async () => {},
|
|
165
|
+
upsertSkillsIndex: () => {},
|
|
166
|
+
getRepoSkillsDir: () => undefined,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
mock.module("../skills/catalog-search.js", () => ({
|
|
170
|
+
filterByQuery: () => [],
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
mock.module("../skills/clawhub.js", () => ({
|
|
174
|
+
clawhubCheckUpdates: mock(async () => []),
|
|
175
|
+
clawhubInspect: mock(async () => ({})),
|
|
176
|
+
clawhubInstall: mock(async () => ({ success: true })),
|
|
177
|
+
clawhubSearch: mock(async () => ({ skills: [] })),
|
|
178
|
+
clawhubUpdate: mock(async () => ({ success: true })),
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
mock.module("../skills/skillssh-registry.js", () => ({
|
|
182
|
+
installExternalSkill: mock(async () => {}),
|
|
183
|
+
resolveSkillSource: () => {
|
|
184
|
+
throw new Error("not used");
|
|
185
|
+
},
|
|
186
|
+
searchSkillsRegistry: mock(async () => []),
|
|
187
|
+
}));
|
|
188
|
+
|
|
189
|
+
mock.module("../skills/install-meta.js", () => ({
|
|
190
|
+
readInstallMeta: () => null,
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
mock.module("../skills/managed-store.js", () => ({
|
|
194
|
+
createManagedSkill: () => ({ created: true }),
|
|
195
|
+
deleteManagedSkill: () => ({ deleted: true }),
|
|
196
|
+
removeSkillsIndexEntry: () => {},
|
|
197
|
+
validateManagedSkillId: () => null,
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
mock.module("../memory/graph/capability-seed.js", () => ({
|
|
201
|
+
deleteSkillCapabilityNode: () => {},
|
|
202
|
+
seedSkillGraphNodes: () => {},
|
|
203
|
+
seedUninstalledCatalogSkillMemories: async () => {},
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
mock.module("../providers/provider-send-message.js", () => ({
|
|
207
|
+
createTimeout: () => ({
|
|
208
|
+
signal: AbortSignal.timeout(1000),
|
|
209
|
+
cleanup: () => {},
|
|
210
|
+
}),
|
|
211
|
+
extractText: () => "",
|
|
212
|
+
getConfiguredProvider: async () => null,
|
|
213
|
+
userMessage: () => ({}),
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
// Real isTextMimeType — we want actual classification here.
|
|
217
|
+
// No mock needed; let it fall through to the real implementation.
|
|
218
|
+
|
|
219
|
+
mock.module("../util/platform.js", () => ({
|
|
220
|
+
getWorkspaceSkillsDir: () => "/tmp/test-skills",
|
|
221
|
+
readPlatformToken: () => null,
|
|
222
|
+
}));
|
|
223
|
+
mock.module("../util/platform.ts", () => ({
|
|
224
|
+
getWorkspaceSkillsDir: () => "/tmp/test-skills",
|
|
225
|
+
readPlatformToken: () => null,
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
let mockPlatformBaseUrl = "https://platform.test";
|
|
229
|
+
mock.module("../config/env.js", () => ({
|
|
230
|
+
getPlatformBaseUrl: () => mockPlatformBaseUrl,
|
|
231
|
+
}));
|
|
232
|
+
mock.module("../config/env.ts", () => ({
|
|
233
|
+
getPlatformBaseUrl: () => mockPlatformBaseUrl,
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
mock.module("../daemon/handlers/shared.js", () => ({
|
|
237
|
+
CONFIG_RELOAD_DEBOUNCE_MS: 100,
|
|
238
|
+
ensureSkillEntry: () => ({ enabled: false }),
|
|
239
|
+
log: noopLogger,
|
|
240
|
+
}));
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Imports (after mocks)
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
import type { SkillOperationContext } from "../daemon/handlers/skills.js";
|
|
247
|
+
import { getSkillFileContent } from "../daemon/handlers/skills.js";
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Helpers
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
const dummyCtx = {
|
|
254
|
+
debounceTimers: { schedule: () => {} },
|
|
255
|
+
setSuppressConfigReload: () => {},
|
|
256
|
+
updateConfigFingerprint: () => {},
|
|
257
|
+
broadcast: () => {},
|
|
258
|
+
} as unknown as SkillOperationContext;
|
|
259
|
+
|
|
260
|
+
type FetchFn = typeof globalThis.fetch;
|
|
261
|
+
|
|
262
|
+
let originalFetch: FetchFn;
|
|
263
|
+
|
|
264
|
+
function installFetchForbidden(): void {
|
|
265
|
+
globalThis.fetch = (async () => {
|
|
266
|
+
throw new Error("fetch should not have been called");
|
|
267
|
+
}) as unknown as FetchFn;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const tempDirs: string[] = [];
|
|
271
|
+
|
|
272
|
+
function makeTempSkillDir(skillId: string): string {
|
|
273
|
+
const root = mkdtempSync(join(tmpdir(), "skill-file-content-test-"));
|
|
274
|
+
tempDirs.push(root);
|
|
275
|
+
const skillDir = join(root, skillId);
|
|
276
|
+
mkdirSync(skillDir, { recursive: true });
|
|
277
|
+
return skillDir;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function writeFile(dir: string, relPath: string, content: string | Buffer) {
|
|
281
|
+
const abs = join(dir, relPath);
|
|
282
|
+
mkdirSync(join(abs, ".."), { recursive: true });
|
|
283
|
+
writeFileSync(abs, content);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function installedSkill(id: string, directoryPath: string) {
|
|
287
|
+
return {
|
|
288
|
+
summary: {
|
|
289
|
+
id,
|
|
290
|
+
name: id,
|
|
291
|
+
displayName: id,
|
|
292
|
+
description: id,
|
|
293
|
+
directoryPath,
|
|
294
|
+
skillFilePath: join(directoryPath, "SKILL.md"),
|
|
295
|
+
source: "workspace" as const,
|
|
296
|
+
},
|
|
297
|
+
state: "enabled" as const,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function catalogSkill(id: string): CatalogSkill {
|
|
302
|
+
return { id, name: id, description: id };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ---------------------------------------------------------------------------
|
|
306
|
+
// Setup / teardown
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
beforeEach(() => {
|
|
310
|
+
originalFetch = globalThis.fetch;
|
|
311
|
+
mockResolvedSkills = [];
|
|
312
|
+
mockCatalog = [];
|
|
313
|
+
mockPlatformBaseUrl = "https://platform.test";
|
|
314
|
+
catalogFileContentCalls.length = 0;
|
|
315
|
+
mockCatalogFileContentResponder = null;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
afterEach(() => {
|
|
319
|
+
globalThis.fetch = originalFetch;
|
|
320
|
+
for (const dir of tempDirs) {
|
|
321
|
+
try {
|
|
322
|
+
rmSync(dir, { recursive: true, force: true });
|
|
323
|
+
} catch {
|
|
324
|
+
// best effort
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
tempDirs.length = 0;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Installed-skill path
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
describe("getSkillFileContent — installed skill", () => {
|
|
335
|
+
test("returns text file content for a valid path", async () => {
|
|
336
|
+
const skillDir = makeTempSkillDir("my-skill");
|
|
337
|
+
writeFile(skillDir, "SKILL.md", "# hello world\n");
|
|
338
|
+
mockResolvedSkills = [installedSkill("my-skill", skillDir)];
|
|
339
|
+
installFetchForbidden();
|
|
340
|
+
|
|
341
|
+
const result = await getSkillFileContent("my-skill", "SKILL.md", dummyCtx);
|
|
342
|
+
expect("error" in result).toBe(false);
|
|
343
|
+
if ("error" in result) return;
|
|
344
|
+
expect(result.path).toBe("SKILL.md");
|
|
345
|
+
expect(result.name).toBe("SKILL.md");
|
|
346
|
+
expect(result.size).toBe("# hello world\n".length);
|
|
347
|
+
expect(result.isBinary).toBe(false);
|
|
348
|
+
expect(result.content).toBe("# hello world\n");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("returns content=null for binary files", async () => {
|
|
352
|
+
const skillDir = makeTempSkillDir("my-skill");
|
|
353
|
+
writeFile(
|
|
354
|
+
skillDir,
|
|
355
|
+
"img.png",
|
|
356
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
357
|
+
);
|
|
358
|
+
mockResolvedSkills = [installedSkill("my-skill", skillDir)];
|
|
359
|
+
installFetchForbidden();
|
|
360
|
+
|
|
361
|
+
const result = await getSkillFileContent("my-skill", "img.png", dummyCtx);
|
|
362
|
+
expect("error" in result).toBe(false);
|
|
363
|
+
if ("error" in result) return;
|
|
364
|
+
expect(result.isBinary).toBe(true);
|
|
365
|
+
expect(result.content).toBeNull();
|
|
366
|
+
expect(result.name).toBe("img.png");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("rejects traversal paths with 400 Invalid path", async () => {
|
|
370
|
+
const skillDir = makeTempSkillDir("my-skill");
|
|
371
|
+
writeFile(skillDir, "SKILL.md", "ok");
|
|
372
|
+
mockResolvedSkills = [installedSkill("my-skill", skillDir)];
|
|
373
|
+
installFetchForbidden();
|
|
374
|
+
|
|
375
|
+
for (const bad of ["../secrets", "..", "/etc/passwd", "./../escape"]) {
|
|
376
|
+
const result = await getSkillFileContent("my-skill", bad, dummyCtx);
|
|
377
|
+
expect("error" in result).toBe(true);
|
|
378
|
+
if (!("error" in result)) continue;
|
|
379
|
+
expect(result.status).toBe(400);
|
|
380
|
+
expect(result.error).toBe("Invalid path");
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("rejects paths containing null bytes with 400", async () => {
|
|
385
|
+
const skillDir = makeTempSkillDir("my-skill");
|
|
386
|
+
writeFile(skillDir, "SKILL.md", "ok");
|
|
387
|
+
mockResolvedSkills = [installedSkill("my-skill", skillDir)];
|
|
388
|
+
installFetchForbidden();
|
|
389
|
+
|
|
390
|
+
const result = await getSkillFileContent(
|
|
391
|
+
"my-skill",
|
|
392
|
+
"SKILL.md\0.png",
|
|
393
|
+
dummyCtx,
|
|
394
|
+
);
|
|
395
|
+
expect("error" in result).toBe(true);
|
|
396
|
+
if (!("error" in result)) return;
|
|
397
|
+
expect(result.status).toBe(400);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("returns 404 for a missing file inside an installed skill", async () => {
|
|
401
|
+
const skillDir = makeTempSkillDir("my-skill");
|
|
402
|
+
writeFile(skillDir, "SKILL.md", "ok");
|
|
403
|
+
mockResolvedSkills = [installedSkill("my-skill", skillDir)];
|
|
404
|
+
installFetchForbidden();
|
|
405
|
+
|
|
406
|
+
const result = await getSkillFileContent("my-skill", "ghost.txt", dummyCtx);
|
|
407
|
+
expect("error" in result).toBe(true);
|
|
408
|
+
if (!("error" in result)) return;
|
|
409
|
+
expect(result.status).toBe(404);
|
|
410
|
+
expect(result.error).toBe("File not found");
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Catalog fallback — helper invocation
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
//
|
|
418
|
+
// The daemon handler delegates to `readCatalogSkillFileContent` for any
|
|
419
|
+
// skill id that is not resolved locally but is present in the platform
|
|
420
|
+
// catalog. The helper's own dev-mode / platform-mode behavior is covered
|
|
421
|
+
// in detail in `catalog-files.test.ts`; these tests assert the handler
|
|
422
|
+
// wiring: the helper is invoked with the sanitized path, and the
|
|
423
|
+
// helper's result is returned on the response shape.
|
|
424
|
+
|
|
425
|
+
describe("getSkillFileContent — uninstalled catalog skill", () => {
|
|
426
|
+
test("delegates to readCatalogSkillFileContent and returns the helper payload", async () => {
|
|
427
|
+
// Skill is NOT in the installed catalog, but IS in the platform catalog.
|
|
428
|
+
mockResolvedSkills = [];
|
|
429
|
+
mockCatalog = [catalogSkill("remote-skill")];
|
|
430
|
+
installFetchForbidden();
|
|
431
|
+
|
|
432
|
+
mockCatalogFileContentResponder = async (_skillId, path) => ({
|
|
433
|
+
path,
|
|
434
|
+
name: "SKILL.md",
|
|
435
|
+
size: 14,
|
|
436
|
+
mimeType: "text/markdown",
|
|
437
|
+
isBinary: false,
|
|
438
|
+
content: "# hello world\n",
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const result = await getSkillFileContent(
|
|
442
|
+
"remote-skill",
|
|
443
|
+
"SKILL.md",
|
|
444
|
+
dummyCtx,
|
|
445
|
+
);
|
|
446
|
+
expect("error" in result).toBe(false);
|
|
447
|
+
if ("error" in result) return;
|
|
448
|
+
expect(result.path).toBe("SKILL.md");
|
|
449
|
+
expect(result.name).toBe("SKILL.md");
|
|
450
|
+
expect(result.size).toBe(14);
|
|
451
|
+
expect(result.mimeType).toBe("text/markdown");
|
|
452
|
+
expect(result.isBinary).toBe(false);
|
|
453
|
+
expect(result.content).toBe("# hello world\n");
|
|
454
|
+
|
|
455
|
+
expect(catalogFileContentCalls).toEqual([
|
|
456
|
+
{ skillId: "remote-skill", path: "SKILL.md" },
|
|
457
|
+
]);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("returns 404 when readCatalogSkillFileContent resolves to null", async () => {
|
|
461
|
+
// Catalog helper returns null for a missing or unreadable file —
|
|
462
|
+
// daemon handler translates that to 404 "File not found".
|
|
463
|
+
mockResolvedSkills = [];
|
|
464
|
+
mockCatalog = [catalogSkill("remote-skill")];
|
|
465
|
+
installFetchForbidden();
|
|
466
|
+
|
|
467
|
+
mockCatalogFileContentResponder = async () => null;
|
|
468
|
+
|
|
469
|
+
const result = await getSkillFileContent(
|
|
470
|
+
"remote-skill",
|
|
471
|
+
"missing.md",
|
|
472
|
+
dummyCtx,
|
|
473
|
+
);
|
|
474
|
+
expect("error" in result).toBe(true);
|
|
475
|
+
if (!("error" in result)) return;
|
|
476
|
+
expect(result.status).toBe(404);
|
|
477
|
+
expect(result.error).toBe("File not found");
|
|
478
|
+
expect(catalogFileContentCalls).toEqual([
|
|
479
|
+
{ skillId: "remote-skill", path: "missing.md" },
|
|
480
|
+
]);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
485
|
+
// Skill not found anywhere
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
|
|
488
|
+
describe("getSkillFileContent — skill not found", () => {
|
|
489
|
+
test("returns 404 when the skill is neither installed nor in the catalog", async () => {
|
|
490
|
+
mockResolvedSkills = [];
|
|
491
|
+
mockCatalog = [];
|
|
492
|
+
installFetchForbidden();
|
|
493
|
+
|
|
494
|
+
const result = await getSkillFileContent(
|
|
495
|
+
"ghost-skill",
|
|
496
|
+
"SKILL.md",
|
|
497
|
+
dummyCtx,
|
|
498
|
+
);
|
|
499
|
+
expect("error" in result).toBe(true);
|
|
500
|
+
if (!("error" in result)) return;
|
|
501
|
+
expect(result.status).toBe(404);
|
|
502
|
+
expect(result.error).toBe("Skill not found");
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
// Installed skill with missing directory (ghost install)
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
//
|
|
510
|
+
// When `findSkillById` resolves a skill as installed but the on-disk
|
|
511
|
+
// directory has disappeared (corrupted install, mid-delete race,
|
|
512
|
+
// external unmount), the handler must return a distinct 404 "Skill
|
|
513
|
+
// directory missing" instead of falling through to the catalog path.
|
|
514
|
+
// Falling through would flip the content response to a catalog payload
|
|
515
|
+
// even though `listSkillsWithCatalog` still classifies the same id as
|
|
516
|
+
// `kind: "installed"`, breaking the `isInstalled` contract between the
|
|
517
|
+
// listing and content responses. Mirrors the same fix on
|
|
518
|
+
// `getSkillFiles` verified by
|
|
519
|
+
// `skills-files-catalog-fallback.test.ts`.
|
|
520
|
+
|
|
521
|
+
describe("getSkillFileContent — installed skill with missing directory", () => {
|
|
522
|
+
test("returns 404 without consulting the catalog when the installed dir is gone", async () => {
|
|
523
|
+
mockResolvedSkills = [
|
|
524
|
+
installedSkill(
|
|
525
|
+
"ghost-installed",
|
|
526
|
+
"/tmp/definitely-does-not-exist-" + Date.now(),
|
|
527
|
+
),
|
|
528
|
+
];
|
|
529
|
+
// Even if the same id is present in the catalog, the handler must NOT
|
|
530
|
+
// fall through. Prime both the catalog and a responder that would
|
|
531
|
+
// return a successful payload to prove the short-circuit is active.
|
|
532
|
+
mockCatalog = [catalogSkill("ghost-installed")];
|
|
533
|
+
mockCatalogFileContentResponder = async () => ({
|
|
534
|
+
path: "SKILL.md",
|
|
535
|
+
name: "SKILL.md",
|
|
536
|
+
size: 10,
|
|
537
|
+
mimeType: "text/markdown",
|
|
538
|
+
isBinary: false,
|
|
539
|
+
content: "# from catalog\n",
|
|
540
|
+
});
|
|
541
|
+
installFetchForbidden();
|
|
542
|
+
|
|
543
|
+
const result = await getSkillFileContent(
|
|
544
|
+
"ghost-installed",
|
|
545
|
+
"SKILL.md",
|
|
546
|
+
dummyCtx,
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
expect("error" in result).toBe(true);
|
|
550
|
+
if (!("error" in result)) return;
|
|
551
|
+
expect(result.status).toBe(404);
|
|
552
|
+
expect(result.error).toContain("ghost-installed");
|
|
553
|
+
expect(result.error).toContain("directory missing");
|
|
554
|
+
// Catalog fallback must not have been consulted.
|
|
555
|
+
expect(catalogFileContentCalls).toEqual([]);
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
// Hidden / SKIP_DIRS path rejection
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
//
|
|
563
|
+
// The daemon handler rejects paths containing dotfile segments (`.env`,
|
|
564
|
+
// `.git/`) or SKIP_DIRS segments (`node_modules`, `__pycache__`) with a
|
|
565
|
+
// 400 "Invalid path" BEFORE any disk read or network round-trip, matching
|
|
566
|
+
// the file-listing endpoint that hides these entries. This applies
|
|
567
|
+
// regardless of whether the skill is installed locally or only available
|
|
568
|
+
// via the catalog.
|
|
569
|
+
|
|
570
|
+
describe("getSkillFileContent — hidden / SKIP_DIRS rejection", () => {
|
|
571
|
+
test("rejects dotfile reads from an installed skill with 400 Invalid path", async () => {
|
|
572
|
+
// Set up an installed skill where a real `.env` file exists on disk.
|
|
573
|
+
// Without the hidden-segment rejection, the handler would happily
|
|
574
|
+
// read its content because sanitizeRelativePath accepts `.env`.
|
|
575
|
+
const skillDir = makeTempSkillDir("leaky-skill");
|
|
576
|
+
writeFile(skillDir, "SKILL.md", "# ok\n");
|
|
577
|
+
writeFile(skillDir, ".env", "SECRET=abc\n");
|
|
578
|
+
mockResolvedSkills = [installedSkill("leaky-skill", skillDir)];
|
|
579
|
+
installFetchForbidden();
|
|
580
|
+
|
|
581
|
+
const result = await getSkillFileContent("leaky-skill", ".env", dummyCtx);
|
|
582
|
+
expect("error" in result).toBe(true);
|
|
583
|
+
if (!("error" in result)) return;
|
|
584
|
+
expect(result.status).toBe(400);
|
|
585
|
+
expect(result.error).toBe("Invalid path");
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("rejects dotfile reads for an uninstalled catalog skill before any catalog read", async () => {
|
|
589
|
+
// Same dotfile attack, but the skill id is NOT installed — only
|
|
590
|
+
// present in the Vellum catalog. The rejection must run in the daemon
|
|
591
|
+
// handler BEFORE the catalog fallback, so the catalog helper should
|
|
592
|
+
// never be consulted. We prime the responder with content that WOULD
|
|
593
|
+
// succeed to prove that even though the fallback path would return a
|
|
594
|
+
// payload, the daemon short-circuits first.
|
|
595
|
+
mockResolvedSkills = []; // not installed
|
|
596
|
+
mockCatalog = [catalogSkill("catalog-leaky")];
|
|
597
|
+
installFetchForbidden();
|
|
598
|
+
mockCatalogFileContentResponder = async () => ({
|
|
599
|
+
path: ".env",
|
|
600
|
+
name: ".env",
|
|
601
|
+
size: 12,
|
|
602
|
+
mimeType: "text/plain",
|
|
603
|
+
isBinary: false,
|
|
604
|
+
content: "SECRET=xyz\n",
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const result = await getSkillFileContent("catalog-leaky", ".env", dummyCtx);
|
|
608
|
+
expect("error" in result).toBe(true);
|
|
609
|
+
if (!("error" in result)) return;
|
|
610
|
+
expect(result.status).toBe(400);
|
|
611
|
+
expect(result.error).toBe("Invalid path");
|
|
612
|
+
// The hidden-segment check must run before the catalog helper.
|
|
613
|
+
expect(catalogFileContentCalls).toEqual([]);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("rejects paths whose parent directory is a dotfile segment", async () => {
|
|
617
|
+
// `.git/config` and `docs/.hidden/file.md` both contain hidden
|
|
618
|
+
// segments even though the leaf isn't a dotfile.
|
|
619
|
+
const skillDir = makeTempSkillDir("my-skill");
|
|
620
|
+
writeFile(skillDir, "SKILL.md", "# ok\n");
|
|
621
|
+
mockResolvedSkills = [installedSkill("my-skill", skillDir)];
|
|
622
|
+
installFetchForbidden();
|
|
623
|
+
|
|
624
|
+
for (const bad of [".git/config", "docs/.hidden/file.md"]) {
|
|
625
|
+
const result = await getSkillFileContent("my-skill", bad, dummyCtx);
|
|
626
|
+
expect("error" in result).toBe(true);
|
|
627
|
+
if (!("error" in result)) continue;
|
|
628
|
+
expect(result.status).toBe(400);
|
|
629
|
+
expect(result.error).toBe("Invalid path");
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test("rejects paths inside SKIP_DIRS segments with 400 Invalid path", async () => {
|
|
634
|
+
const skillDir = makeTempSkillDir("my-skill");
|
|
635
|
+
writeFile(skillDir, "SKILL.md", "# ok\n");
|
|
636
|
+
mockResolvedSkills = [installedSkill("my-skill", skillDir)];
|
|
637
|
+
installFetchForbidden();
|
|
638
|
+
|
|
639
|
+
for (const bad of [
|
|
640
|
+
"node_modules/foo/index.js",
|
|
641
|
+
"__pycache__/cached.pyc",
|
|
642
|
+
"nested/node_modules/mod/index.js",
|
|
643
|
+
]) {
|
|
644
|
+
const result = await getSkillFileContent("my-skill", bad, dummyCtx);
|
|
645
|
+
expect("error" in result).toBe(true);
|
|
646
|
+
if (!("error" in result)) continue;
|
|
647
|
+
expect(result.status).toBe(400);
|
|
648
|
+
expect(result.error).toBe("Invalid path");
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("regular SKILL.md still reads successfully (sanity)", async () => {
|
|
653
|
+
// Guards against collateral damage from the hidden/SKIP_DIRS filter:
|
|
654
|
+
// a normal, non-hidden path must continue to work unchanged.
|
|
655
|
+
const skillDir = makeTempSkillDir("healthy-skill");
|
|
656
|
+
writeFile(skillDir, "SKILL.md", "# hello\n");
|
|
657
|
+
mockResolvedSkills = [installedSkill("healthy-skill", skillDir)];
|
|
658
|
+
installFetchForbidden();
|
|
659
|
+
|
|
660
|
+
const result = await getSkillFileContent(
|
|
661
|
+
"healthy-skill",
|
|
662
|
+
"SKILL.md",
|
|
663
|
+
dummyCtx,
|
|
664
|
+
);
|
|
665
|
+
expect("error" in result).toBe(false);
|
|
666
|
+
if ("error" in result) return;
|
|
667
|
+
expect(result.content).toBe("# hello\n");
|
|
668
|
+
expect(result.name).toBe("SKILL.md");
|
|
669
|
+
});
|
|
670
|
+
});
|