@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,862 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `skills/catalog-files.ts` — preview listings and single-file
|
|
3
|
+
* content for catalog skills (installed or not).
|
|
4
|
+
*
|
|
5
|
+
* Covers:
|
|
6
|
+
* - sanitizeRelativePath (accepts safe, rejects traversal / absolute / null)
|
|
7
|
+
* - readCatalogSkillFiles dev-mode (reads from a temp fake repo skills dir,
|
|
8
|
+
* does NOT touch fetch)
|
|
9
|
+
* - readCatalogSkillFiles platform-mode (stubbed fetch with success + 500
|
|
10
|
+
* + 404 + network-error)
|
|
11
|
+
* - readCatalogSkillFileContent dev-mode (text, traversal rejection,
|
|
12
|
+
* binary, oversized)
|
|
13
|
+
* - readCatalogSkillFileContent platform-mode (text, binary, oversized,
|
|
14
|
+
* 404, pre-fetch traversal rejection)
|
|
15
|
+
* - catalog-miss short-circuit (no fetch call when id unknown)
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
mkdirSync,
|
|
20
|
+
mkdtempSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
symlinkSync,
|
|
23
|
+
writeFileSync,
|
|
24
|
+
} from "node:fs";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
28
|
+
|
|
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
|
+
mock.module("../util/logger.js", () => ({
|
|
37
|
+
getLogger: () =>
|
|
38
|
+
new Proxy({} as Record<string, unknown>, {
|
|
39
|
+
get: () => () => {},
|
|
40
|
+
}),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
let mockCatalog: CatalogSkill[] = [];
|
|
44
|
+
let mockRepoSkillsDir: string | undefined = undefined;
|
|
45
|
+
|
|
46
|
+
mock.module("../skills/catalog-cache.js", () => ({
|
|
47
|
+
getCatalog: async () => mockCatalog,
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
mock.module("../skills/catalog-install.js", () => ({
|
|
51
|
+
getRepoSkillsDir: () => mockRepoSkillsDir,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
let mockPlatformToken: string | null = null;
|
|
55
|
+
mock.module("../util/platform.ts", () => ({
|
|
56
|
+
readPlatformToken: () => mockPlatformToken,
|
|
57
|
+
}));
|
|
58
|
+
mock.module("../util/platform.js", () => ({
|
|
59
|
+
readPlatformToken: () => mockPlatformToken,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
let mockPlatformBaseUrl = "https://platform.test";
|
|
63
|
+
mock.module("../config/env.ts", () => ({
|
|
64
|
+
getPlatformBaseUrl: () => mockPlatformBaseUrl,
|
|
65
|
+
}));
|
|
66
|
+
mock.module("../config/env.js", () => ({
|
|
67
|
+
getPlatformBaseUrl: () => mockPlatformBaseUrl,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Imports (after mocks)
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
import {
|
|
75
|
+
readCatalogSkillFileContent,
|
|
76
|
+
readCatalogSkillFiles,
|
|
77
|
+
sanitizeRelativePath,
|
|
78
|
+
} from "../skills/catalog-files.js";
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Helpers
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
type FetchFn = typeof globalThis.fetch;
|
|
85
|
+
|
|
86
|
+
interface FetchCall {
|
|
87
|
+
url: string;
|
|
88
|
+
init?: RequestInit;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let originalFetch: FetchFn;
|
|
92
|
+
let fetchCalls: FetchCall[] = [];
|
|
93
|
+
|
|
94
|
+
function installFetchMock(
|
|
95
|
+
handler: (url: string, init?: RequestInit) => Response | Promise<Response>,
|
|
96
|
+
): void {
|
|
97
|
+
fetchCalls = [];
|
|
98
|
+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
99
|
+
const url =
|
|
100
|
+
typeof input === "string"
|
|
101
|
+
? input
|
|
102
|
+
: input instanceof URL
|
|
103
|
+
? input.toString()
|
|
104
|
+
: input.url;
|
|
105
|
+
fetchCalls.push({ url, init });
|
|
106
|
+
return handler(url, init);
|
|
107
|
+
}) as unknown as FetchFn;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function installFetchThrow(error: Error): void {
|
|
111
|
+
fetchCalls = [];
|
|
112
|
+
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
113
|
+
const url =
|
|
114
|
+
typeof input === "string"
|
|
115
|
+
? input
|
|
116
|
+
: input instanceof URL
|
|
117
|
+
? input.toString()
|
|
118
|
+
: input.url;
|
|
119
|
+
fetchCalls.push({ url, init });
|
|
120
|
+
throw error;
|
|
121
|
+
}) as unknown as FetchFn;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function installFetchForbidden(): void {
|
|
125
|
+
fetchCalls = [];
|
|
126
|
+
globalThis.fetch = (async () => {
|
|
127
|
+
throw new Error("fetch should not have been called");
|
|
128
|
+
}) as unknown as FetchFn;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Temp directories created during tests, cleaned up in afterEach.
|
|
132
|
+
const tempDirs: string[] = [];
|
|
133
|
+
|
|
134
|
+
function makeTempSkillsDir(): string {
|
|
135
|
+
const dir = mkdtempSync(join(tmpdir(), "catalog-files-test-"));
|
|
136
|
+
tempDirs.push(dir);
|
|
137
|
+
return dir;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function writeSkill(
|
|
141
|
+
root: string,
|
|
142
|
+
skillId: string,
|
|
143
|
+
files: Record<string, string | Buffer>,
|
|
144
|
+
): string {
|
|
145
|
+
const skillDir = join(root, skillId);
|
|
146
|
+
mkdirSync(skillDir, { recursive: true });
|
|
147
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
148
|
+
const abs = join(skillDir, relPath);
|
|
149
|
+
mkdirSync(join(abs, ".."), { recursive: true });
|
|
150
|
+
writeFileSync(abs, content);
|
|
151
|
+
}
|
|
152
|
+
return skillDir;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function skill(id: string): CatalogSkill {
|
|
156
|
+
return { id, name: id, description: id };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Setup / teardown
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
originalFetch = globalThis.fetch;
|
|
165
|
+
fetchCalls = [];
|
|
166
|
+
mockCatalog = [];
|
|
167
|
+
mockRepoSkillsDir = undefined;
|
|
168
|
+
mockPlatformToken = null;
|
|
169
|
+
mockPlatformBaseUrl = "https://platform.test";
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
afterEach(() => {
|
|
173
|
+
globalThis.fetch = originalFetch;
|
|
174
|
+
for (const dir of tempDirs) {
|
|
175
|
+
try {
|
|
176
|
+
rmSync(dir, { recursive: true, force: true });
|
|
177
|
+
} catch {
|
|
178
|
+
// best effort
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
tempDirs.length = 0;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// sanitizeRelativePath
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
describe("sanitizeRelativePath", () => {
|
|
189
|
+
test("accepts simple posix paths", () => {
|
|
190
|
+
expect(sanitizeRelativePath("SKILL.md")).toBe("SKILL.md");
|
|
191
|
+
expect(sanitizeRelativePath("tools/run.sh")).toBe("tools/run.sh");
|
|
192
|
+
expect(sanitizeRelativePath("a/b/c.txt")).toBe("a/b/c.txt");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("normalizes leading ./", () => {
|
|
196
|
+
expect(sanitizeRelativePath("./x")).toBe("x");
|
|
197
|
+
expect(sanitizeRelativePath("./tools/run.sh")).toBe("tools/run.sh");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("normalizes backslashes to forward slashes", () => {
|
|
201
|
+
expect(sanitizeRelativePath("tools\\run.sh")).toBe("tools/run.sh");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("rejects empty strings", () => {
|
|
205
|
+
expect(sanitizeRelativePath("")).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("rejects parent-traversal", () => {
|
|
209
|
+
expect(sanitizeRelativePath("..")).toBeNull();
|
|
210
|
+
expect(sanitizeRelativePath("../x")).toBeNull();
|
|
211
|
+
expect(sanitizeRelativePath("../../etc/passwd")).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("rejects absolute unix paths", () => {
|
|
215
|
+
expect(sanitizeRelativePath("/etc/passwd")).toBeNull();
|
|
216
|
+
expect(sanitizeRelativePath("/")).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("rejects windows drive-prefixed paths", () => {
|
|
220
|
+
expect(sanitizeRelativePath("C:/win")).toBeNull();
|
|
221
|
+
expect(sanitizeRelativePath("C:\\Windows")).toBeNull();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("rejects paths that become absolute after ./ stripping", () => {
|
|
225
|
+
// `sanitizeRelativePath` performs a post-normalization absolute-path
|
|
226
|
+
// check so inputs like `.//etc/passwd` cannot reach the filesystem:
|
|
227
|
+
// the leading `./` strip loop leaves `/etc/passwd`, which
|
|
228
|
+
// `posix.normalize` would otherwise pass through as an absolute path.
|
|
229
|
+
expect(sanitizeRelativePath(".//etc/passwd")).toBeNull();
|
|
230
|
+
expect(sanitizeRelativePath("./././/etc/passwd")).toBeNull();
|
|
231
|
+
// The backslash normalizes to `/`, so `.\\/etc/passwd` becomes
|
|
232
|
+
// `.//etc/passwd` before the strip loop, then `/etc/passwd`.
|
|
233
|
+
expect(sanitizeRelativePath(".\\/etc/passwd")).toBeNull();
|
|
234
|
+
// Windows-drive bypass via the same mechanism.
|
|
235
|
+
expect(sanitizeRelativePath(".//C:/Windows/system32")).toBeNull();
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("rejects paths containing null bytes", () => {
|
|
239
|
+
expect(sanitizeRelativePath("SKILL.md\0.png")).toBeNull();
|
|
240
|
+
expect(sanitizeRelativePath("\0")).toBeNull();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// readCatalogSkillFiles — dev mode
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
describe("readCatalogSkillFiles (dev mode)", () => {
|
|
249
|
+
test("lists files from the repo skills dir without touching fetch", async () => {
|
|
250
|
+
const root = makeTempSkillsDir();
|
|
251
|
+
writeSkill(root, "my-skill", {
|
|
252
|
+
"SKILL.md": "# hello",
|
|
253
|
+
"tools/run.sh": "#!/bin/sh\necho hi\n",
|
|
254
|
+
"data/img.png": Buffer.from([0x89, 0x50, 0x4e, 0x47]),
|
|
255
|
+
});
|
|
256
|
+
mockRepoSkillsDir = root;
|
|
257
|
+
mockCatalog = [skill("my-skill")];
|
|
258
|
+
installFetchForbidden();
|
|
259
|
+
|
|
260
|
+
const entries = await readCatalogSkillFiles("my-skill");
|
|
261
|
+
expect(entries).not.toBeNull();
|
|
262
|
+
const paths = entries!.map((e) => e.path).sort();
|
|
263
|
+
expect(paths).toEqual(["SKILL.md", "data/img.png", "tools/run.sh"]);
|
|
264
|
+
|
|
265
|
+
const md = entries!.find((e) => e.path === "SKILL.md")!;
|
|
266
|
+
expect(md.name).toBe("SKILL.md");
|
|
267
|
+
expect(md.size).toBe("# hello".length);
|
|
268
|
+
expect(md.isBinary).toBe(false);
|
|
269
|
+
expect(md.content).toBeNull();
|
|
270
|
+
|
|
271
|
+
const png = entries!.find((e) => e.path === "data/img.png")!;
|
|
272
|
+
expect(png.name).toBe("img.png");
|
|
273
|
+
expect(png.isBinary).toBe(true);
|
|
274
|
+
expect(png.content).toBeNull();
|
|
275
|
+
|
|
276
|
+
expect(fetchCalls.length).toBe(0);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("returns null when skill id is not in the catalog (dev mode)", async () => {
|
|
280
|
+
const root = makeTempSkillsDir();
|
|
281
|
+
writeSkill(root, "my-skill", { "SKILL.md": "x" });
|
|
282
|
+
mockRepoSkillsDir = root;
|
|
283
|
+
mockCatalog = []; // not in catalog
|
|
284
|
+
installFetchForbidden();
|
|
285
|
+
|
|
286
|
+
expect(await readCatalogSkillFiles("my-skill")).toBeNull();
|
|
287
|
+
expect(fetchCalls.length).toBe(0);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("rejects a symlinked skill root and falls through to platform mode", async () => {
|
|
291
|
+
// An attacker (or a misconfigured dev) creates
|
|
292
|
+
// <repoSkillsDir>/my-skill as a symlink pointing at an external
|
|
293
|
+
// directory. `resolveCatalogSource` rejects symlinked skill roots so
|
|
294
|
+
// the dev-mode branch never walks the external directory — the
|
|
295
|
+
// realpath containment check downstream would otherwise derive
|
|
296
|
+
// `realRoot` from the already-resolved symlink target and become a
|
|
297
|
+
// no-op.
|
|
298
|
+
//
|
|
299
|
+
// Expected behavior: the dev-mode shortcut is rejected up-front, and
|
|
300
|
+
// we fall through to platform mode — which in this test is stubbed
|
|
301
|
+
// to return an empty file list. So `readCatalogSkillFiles` returns
|
|
302
|
+
// the empty platform response, and `fetch` MUST be called (proving
|
|
303
|
+
// the fall-through happened, rather than the dev-mode shortcut
|
|
304
|
+
// silently reading from the external directory).
|
|
305
|
+
const root = makeTempSkillsDir();
|
|
306
|
+
const externalRoot = mkdtempSync(join(tmpdir(), "catalog-files-ext-"));
|
|
307
|
+
tempDirs.push(externalRoot);
|
|
308
|
+
|
|
309
|
+
// External directory populated with a real SKILL.md + a secret file
|
|
310
|
+
// that must NOT be exposed by the listing.
|
|
311
|
+
writeFileSync(join(externalRoot, "SKILL.md"), "# EXTERNAL SKILL");
|
|
312
|
+
writeFileSync(join(externalRoot, "secret.txt"), "EXTERNAL_SECRET");
|
|
313
|
+
|
|
314
|
+
// Symlink the skill root at `<root>/my-skill` to the external dir.
|
|
315
|
+
symlinkSync(externalRoot, join(root, "my-skill"));
|
|
316
|
+
|
|
317
|
+
mockRepoSkillsDir = root;
|
|
318
|
+
mockCatalog = [skill("my-skill")];
|
|
319
|
+
installFetchMock(() => Response.json({ skill_id: "my-skill", files: [] }));
|
|
320
|
+
|
|
321
|
+
const entries = await readCatalogSkillFiles("my-skill");
|
|
322
|
+
|
|
323
|
+
// Fall-through should produce the platform response (empty array),
|
|
324
|
+
// NOT the external directory contents.
|
|
325
|
+
expect(entries).toEqual([]);
|
|
326
|
+
|
|
327
|
+
// Confirm the dev-mode shortcut was bypassed: platform fetch was
|
|
328
|
+
// called against the preview endpoint.
|
|
329
|
+
expect(fetchCalls.length).toBe(1);
|
|
330
|
+
expect(fetchCalls[0]!.url).toBe(
|
|
331
|
+
"https://platform.test/v1/skills/my-skill/files/",
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("rejects a symlinked skill root for content reads and falls through to platform mode", async () => {
|
|
336
|
+
// Direct reproduction for `readCatalogSkillFileContent`: with a
|
|
337
|
+
// symlinked skill root, the dev branch must not read the external
|
|
338
|
+
// file. Instead we should fall through to the platform endpoint and
|
|
339
|
+
// return whatever the platform says.
|
|
340
|
+
const root = makeTempSkillsDir();
|
|
341
|
+
const externalRoot = mkdtempSync(join(tmpdir(), "catalog-files-ext-"));
|
|
342
|
+
tempDirs.push(externalRoot);
|
|
343
|
+
|
|
344
|
+
writeFileSync(join(externalRoot, "SKILL.md"), "EXTERNAL_SKILL_CONTENT");
|
|
345
|
+
symlinkSync(externalRoot, join(root, "my-skill"));
|
|
346
|
+
|
|
347
|
+
mockRepoSkillsDir = root;
|
|
348
|
+
mockCatalog = [skill("my-skill")];
|
|
349
|
+
installFetchMock(() =>
|
|
350
|
+
Response.json({
|
|
351
|
+
path: "SKILL.md",
|
|
352
|
+
name: "SKILL.md",
|
|
353
|
+
size: 14,
|
|
354
|
+
mime_type: "text/markdown",
|
|
355
|
+
is_binary: false,
|
|
356
|
+
content: "PLATFORM_CONTENT",
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const entry = await readCatalogSkillFileContent("my-skill", "SKILL.md");
|
|
361
|
+
expect(entry).not.toBeNull();
|
|
362
|
+
// Content must be the platform payload, NOT the external file's
|
|
363
|
+
// bytes — otherwise the fall-through didn't happen.
|
|
364
|
+
expect(entry!.content).toBe("PLATFORM_CONTENT");
|
|
365
|
+
expect(entry!.mimeType).toBe("text/markdown");
|
|
366
|
+
|
|
367
|
+
// And fetch was called, confirming dev-mode was bypassed.
|
|
368
|
+
expect(fetchCalls.length).toBe(1);
|
|
369
|
+
const url = fetchCalls[0]!.url;
|
|
370
|
+
expect(
|
|
371
|
+
url.startsWith("https://platform.test/v1/skills/my-skill/files/content/"),
|
|
372
|
+
).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
test("non-symlinked skill root still uses the dev-mode shortcut", async () => {
|
|
376
|
+
// Sanity check: a normal directory-based skill root must still be
|
|
377
|
+
// served from disk without touching fetch. This guards against the
|
|
378
|
+
// symlink-rejection path collateral-damaging regular dev flows.
|
|
379
|
+
const root = makeTempSkillsDir();
|
|
380
|
+
writeSkill(root, "normal-skill", { "SKILL.md": "# normal\n" });
|
|
381
|
+
mockRepoSkillsDir = root;
|
|
382
|
+
mockCatalog = [skill("normal-skill")];
|
|
383
|
+
installFetchForbidden();
|
|
384
|
+
|
|
385
|
+
const entries = await readCatalogSkillFiles("normal-skill");
|
|
386
|
+
expect(entries).not.toBeNull();
|
|
387
|
+
const paths = entries!.map((e) => e.path).sort();
|
|
388
|
+
expect(paths).toEqual(["SKILL.md"]);
|
|
389
|
+
// No platform round-trip happened.
|
|
390
|
+
expect(fetchCalls.length).toBe(0);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("filters hidden files and SKIP_DIRS from the listing", async () => {
|
|
394
|
+
// Simulates a dev working on a catalog skill locally who has a
|
|
395
|
+
// node_modules/, a .git/, and a .hidden.md file sitting next to
|
|
396
|
+
// SKILL.md. The preview listing must only show SKILL.md — matching
|
|
397
|
+
// the behavior of `readDirRecursive` in `daemon/handlers/skills.ts`
|
|
398
|
+
// for installed skills.
|
|
399
|
+
const root = makeTempSkillsDir();
|
|
400
|
+
writeSkill(root, "my-skill", {
|
|
401
|
+
"SKILL.md": "# hello",
|
|
402
|
+
"node_modules/foo.js": "module.exports = {};",
|
|
403
|
+
"node_modules/nested/bar.js": "module.exports = {};",
|
|
404
|
+
"__pycache__/cached.pyc": Buffer.from([0x00, 0x01, 0x02]),
|
|
405
|
+
".git/HEAD": "ref: refs/heads/main\n",
|
|
406
|
+
".git/config": "[core]\n",
|
|
407
|
+
".hidden.md": "secret",
|
|
408
|
+
".DS_Store": Buffer.from([0x00, 0x00]),
|
|
409
|
+
});
|
|
410
|
+
mockRepoSkillsDir = root;
|
|
411
|
+
mockCatalog = [skill("my-skill")];
|
|
412
|
+
installFetchForbidden();
|
|
413
|
+
|
|
414
|
+
const entries = await readCatalogSkillFiles("my-skill");
|
|
415
|
+
expect(entries).not.toBeNull();
|
|
416
|
+
const paths = entries!.map((e) => e.path).sort();
|
|
417
|
+
expect(paths).toEqual(["SKILL.md"]);
|
|
418
|
+
expect(fetchCalls.length).toBe(0);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
// readCatalogSkillFiles — platform mode
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
describe("readCatalogSkillFiles (platform mode)", () => {
|
|
427
|
+
test("fetches file listing from the platform and maps it", async () => {
|
|
428
|
+
mockRepoSkillsDir = undefined;
|
|
429
|
+
mockCatalog = [skill("remote-skill")];
|
|
430
|
+
mockPlatformToken = "tok-123";
|
|
431
|
+
installFetchMock(() =>
|
|
432
|
+
Response.json({
|
|
433
|
+
skill_id: "remote-skill",
|
|
434
|
+
files: [
|
|
435
|
+
{ path: "SKILL.md", name: "SKILL.md", size: 12, sha: "a" },
|
|
436
|
+
{ path: "data/img.png", name: "img.png", size: 200, sha: "b" },
|
|
437
|
+
],
|
|
438
|
+
}),
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const entries = await readCatalogSkillFiles("remote-skill");
|
|
442
|
+
expect(entries).not.toBeNull();
|
|
443
|
+
expect(entries!.length).toBe(2);
|
|
444
|
+
|
|
445
|
+
// URL + headers
|
|
446
|
+
expect(fetchCalls.length).toBe(1);
|
|
447
|
+
expect(fetchCalls[0]!.url).toBe(
|
|
448
|
+
"https://platform.test/v1/skills/remote-skill/files/",
|
|
449
|
+
);
|
|
450
|
+
const headers = (fetchCalls[0]!.init?.headers ?? {}) as Record<
|
|
451
|
+
string,
|
|
452
|
+
string
|
|
453
|
+
>;
|
|
454
|
+
expect(headers["Accept"]).toBe("application/json");
|
|
455
|
+
expect(headers["X-Conversation-Token"]).toBe("tok-123");
|
|
456
|
+
|
|
457
|
+
// Mapped entries: always content === null, isBinary from filename.
|
|
458
|
+
const md = entries!.find((e) => e.path === "SKILL.md")!;
|
|
459
|
+
expect(md.isBinary).toBe(false);
|
|
460
|
+
expect(md.content).toBeNull();
|
|
461
|
+
expect(md.mimeType).toBe("");
|
|
462
|
+
|
|
463
|
+
const png = entries!.find((e) => e.path === "data/img.png")!;
|
|
464
|
+
expect(png.isBinary).toBe(true);
|
|
465
|
+
expect(png.content).toBeNull();
|
|
466
|
+
expect(png.mimeType).toBe("");
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("does not set X-Conversation-Token when no token is present", async () => {
|
|
470
|
+
mockCatalog = [skill("remote-skill")];
|
|
471
|
+
mockPlatformToken = null;
|
|
472
|
+
installFetchMock(() =>
|
|
473
|
+
Response.json({ skill_id: "remote-skill", files: [] }),
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
await readCatalogSkillFiles("remote-skill");
|
|
477
|
+
const headers = (fetchCalls[0]!.init?.headers ?? {}) as Record<
|
|
478
|
+
string,
|
|
479
|
+
string
|
|
480
|
+
>;
|
|
481
|
+
expect(headers["X-Conversation-Token"]).toBeUndefined();
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("returns null on 500", async () => {
|
|
485
|
+
mockCatalog = [skill("remote-skill")];
|
|
486
|
+
installFetchMock(
|
|
487
|
+
() => new Response("boom", { status: 500, statusText: "Server Error" }),
|
|
488
|
+
);
|
|
489
|
+
expect(await readCatalogSkillFiles("remote-skill")).toBeNull();
|
|
490
|
+
expect(fetchCalls.length).toBe(1);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
test("returns null on 404", async () => {
|
|
494
|
+
mockCatalog = [skill("remote-skill")];
|
|
495
|
+
installFetchMock(
|
|
496
|
+
() => new Response("missing", { status: 404, statusText: "Not Found" }),
|
|
497
|
+
);
|
|
498
|
+
expect(await readCatalogSkillFiles("remote-skill")).toBeNull();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("returns null on network error", async () => {
|
|
502
|
+
mockCatalog = [skill("remote-skill")];
|
|
503
|
+
installFetchThrow(new Error("ECONNRESET"));
|
|
504
|
+
expect(await readCatalogSkillFiles("remote-skill")).toBeNull();
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("returns null without fetching when skill id missing from catalog", async () => {
|
|
508
|
+
mockCatalog = [];
|
|
509
|
+
installFetchForbidden();
|
|
510
|
+
expect(await readCatalogSkillFiles("unknown")).toBeNull();
|
|
511
|
+
expect(fetchCalls.length).toBe(0);
|
|
512
|
+
});
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ---------------------------------------------------------------------------
|
|
516
|
+
// readCatalogSkillFileContent — dev mode
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
|
|
519
|
+
describe("readCatalogSkillFileContent (dev mode)", () => {
|
|
520
|
+
test("returns inline UTF-8 content for a text file", async () => {
|
|
521
|
+
const root = makeTempSkillsDir();
|
|
522
|
+
writeSkill(root, "my-skill", { "SKILL.md": "# hello world\n" });
|
|
523
|
+
mockRepoSkillsDir = root;
|
|
524
|
+
mockCatalog = [skill("my-skill")];
|
|
525
|
+
installFetchForbidden();
|
|
526
|
+
|
|
527
|
+
const entry = await readCatalogSkillFileContent("my-skill", "SKILL.md");
|
|
528
|
+
expect(entry).not.toBeNull();
|
|
529
|
+
expect(entry!.path).toBe("SKILL.md");
|
|
530
|
+
expect(entry!.name).toBe("SKILL.md");
|
|
531
|
+
expect(entry!.isBinary).toBe(false);
|
|
532
|
+
expect(entry!.content).toBe("# hello world\n");
|
|
533
|
+
expect(fetchCalls.length).toBe(0);
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
test("rejects traversal paths without touching the filesystem", async () => {
|
|
537
|
+
const root = makeTempSkillsDir();
|
|
538
|
+
writeSkill(root, "my-skill", { "SKILL.md": "ok" });
|
|
539
|
+
mockRepoSkillsDir = root;
|
|
540
|
+
mockCatalog = [skill("my-skill")];
|
|
541
|
+
installFetchForbidden();
|
|
542
|
+
|
|
543
|
+
expect(
|
|
544
|
+
await readCatalogSkillFileContent("my-skill", "../escape"),
|
|
545
|
+
).toBeNull();
|
|
546
|
+
expect(
|
|
547
|
+
await readCatalogSkillFileContent("my-skill", "/etc/passwd"),
|
|
548
|
+
).toBeNull();
|
|
549
|
+
expect(await readCatalogSkillFileContent("my-skill", "..")).toBeNull();
|
|
550
|
+
expect(await readCatalogSkillFileContent("my-skill", "")).toBeNull();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("returns content=null for a binary file", async () => {
|
|
554
|
+
const root = makeTempSkillsDir();
|
|
555
|
+
writeSkill(root, "my-skill", {
|
|
556
|
+
"img.png": Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
557
|
+
});
|
|
558
|
+
mockRepoSkillsDir = root;
|
|
559
|
+
mockCatalog = [skill("my-skill")];
|
|
560
|
+
installFetchForbidden();
|
|
561
|
+
|
|
562
|
+
const entry = await readCatalogSkillFileContent("my-skill", "img.png");
|
|
563
|
+
expect(entry).not.toBeNull();
|
|
564
|
+
expect(entry!.isBinary).toBe(true);
|
|
565
|
+
expect(entry!.content).toBeNull();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
test("returns content=null for oversized text files", async () => {
|
|
569
|
+
const root = makeTempSkillsDir();
|
|
570
|
+
// Just over 2 MB of 'a'
|
|
571
|
+
const oversized = "a".repeat(2 * 1024 * 1024 + 1);
|
|
572
|
+
writeSkill(root, "my-skill", { "big.txt": oversized });
|
|
573
|
+
mockRepoSkillsDir = root;
|
|
574
|
+
mockCatalog = [skill("my-skill")];
|
|
575
|
+
installFetchForbidden();
|
|
576
|
+
|
|
577
|
+
const entry = await readCatalogSkillFileContent("my-skill", "big.txt");
|
|
578
|
+
expect(entry).not.toBeNull();
|
|
579
|
+
expect(entry!.isBinary).toBe(false);
|
|
580
|
+
expect(entry!.content).toBeNull();
|
|
581
|
+
expect(entry!.size).toBe(oversized.length);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
test("returns null for a missing file", async () => {
|
|
585
|
+
const root = makeTempSkillsDir();
|
|
586
|
+
writeSkill(root, "my-skill", { "SKILL.md": "ok" });
|
|
587
|
+
mockRepoSkillsDir = root;
|
|
588
|
+
mockCatalog = [skill("my-skill")];
|
|
589
|
+
installFetchForbidden();
|
|
590
|
+
|
|
591
|
+
expect(
|
|
592
|
+
await readCatalogSkillFileContent("my-skill", "does/not/exist.txt"),
|
|
593
|
+
).toBeNull();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("returns null without fetching when skill id missing from catalog", async () => {
|
|
597
|
+
mockCatalog = [];
|
|
598
|
+
installFetchForbidden();
|
|
599
|
+
expect(await readCatalogSkillFileContent("unknown", "SKILL.md")).toBeNull();
|
|
600
|
+
expect(fetchCalls.length).toBe(0);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("rejects symlinked files that point outside the skill root", async () => {
|
|
604
|
+
// Create a temp skill root AND a separate external directory.
|
|
605
|
+
const root = makeTempSkillsDir();
|
|
606
|
+
const externalRoot = mkdtempSync(join(tmpdir(), "catalog-files-ext-"));
|
|
607
|
+
tempDirs.push(externalRoot);
|
|
608
|
+
|
|
609
|
+
// Write an "external secret" file completely outside the skill tree.
|
|
610
|
+
const externalSecret = join(externalRoot, "secret.txt");
|
|
611
|
+
writeFileSync(externalSecret, "EXTERNAL_SECRET");
|
|
612
|
+
|
|
613
|
+
// Create the skill directory itself with a legitimate file.
|
|
614
|
+
const skillDir = writeSkill(root, "my-skill", { "SKILL.md": "ok" });
|
|
615
|
+
|
|
616
|
+
// Create a symlink INSIDE the skill dir pointing at the external file.
|
|
617
|
+
const linkPath = join(skillDir, "link-to-secret.md");
|
|
618
|
+
symlinkSync(externalSecret, linkPath);
|
|
619
|
+
|
|
620
|
+
mockRepoSkillsDir = root;
|
|
621
|
+
mockCatalog = [skill("my-skill")];
|
|
622
|
+
installFetchForbidden();
|
|
623
|
+
|
|
624
|
+
const entry = await readCatalogSkillFileContent(
|
|
625
|
+
"my-skill",
|
|
626
|
+
"link-to-secret.md",
|
|
627
|
+
);
|
|
628
|
+
expect(entry).toBeNull();
|
|
629
|
+
|
|
630
|
+
// And the legitimate file is still readable, so the check didn't
|
|
631
|
+
// collateral-damage normal requests.
|
|
632
|
+
const ok = await readCatalogSkillFileContent("my-skill", "SKILL.md");
|
|
633
|
+
expect(ok).not.toBeNull();
|
|
634
|
+
expect(ok!.content).toBe("ok");
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("rejects files accessed through a symlinked parent directory", async () => {
|
|
638
|
+
const root = makeTempSkillsDir();
|
|
639
|
+
const externalRoot = mkdtempSync(join(tmpdir(), "catalog-files-ext-"));
|
|
640
|
+
tempDirs.push(externalRoot);
|
|
641
|
+
|
|
642
|
+
// External directory with a real file inside it.
|
|
643
|
+
const externalDir = join(externalRoot, "external-dir");
|
|
644
|
+
mkdirSync(externalDir, { recursive: true });
|
|
645
|
+
writeFileSync(join(externalDir, "payload.txt"), "EXTERNAL_PAYLOAD");
|
|
646
|
+
|
|
647
|
+
// Legitimate skill dir with a normal file.
|
|
648
|
+
const skillDir = writeSkill(root, "my-skill", { "SKILL.md": "ok" });
|
|
649
|
+
|
|
650
|
+
// Inside the skill dir, create a symlinked subdirectory that points at
|
|
651
|
+
// the external directory. Then try to request
|
|
652
|
+
// `escape/payload.txt` — lexically this is inside the skill root, but
|
|
653
|
+
// the physical file lives outside.
|
|
654
|
+
const escapeLink = join(skillDir, "escape");
|
|
655
|
+
symlinkSync(externalDir, escapeLink);
|
|
656
|
+
|
|
657
|
+
mockRepoSkillsDir = root;
|
|
658
|
+
mockCatalog = [skill("my-skill")];
|
|
659
|
+
installFetchForbidden();
|
|
660
|
+
|
|
661
|
+
const entry = await readCatalogSkillFileContent(
|
|
662
|
+
"my-skill",
|
|
663
|
+
"escape/payload.txt",
|
|
664
|
+
);
|
|
665
|
+
expect(entry).toBeNull();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test("rejects dotfile paths and returns null without reading disk", async () => {
|
|
669
|
+
// `.env` is a valid sanitized path (sanitizeRelativePath accepts it),
|
|
670
|
+
// but the hidden-segment check must reject it so the catalog content
|
|
671
|
+
// reader never exposes dotfiles, matching the listing API that hides
|
|
672
|
+
// them. This preserves parity between listing and content endpoints.
|
|
673
|
+
const root = makeTempSkillsDir();
|
|
674
|
+
writeSkill(root, "my-skill", {
|
|
675
|
+
"SKILL.md": "ok",
|
|
676
|
+
".env": "SECRET=abc\n",
|
|
677
|
+
});
|
|
678
|
+
mockRepoSkillsDir = root;
|
|
679
|
+
mockCatalog = [skill("my-skill")];
|
|
680
|
+
installFetchForbidden();
|
|
681
|
+
|
|
682
|
+
expect(await readCatalogSkillFileContent("my-skill", ".env")).toBeNull();
|
|
683
|
+
expect(
|
|
684
|
+
await readCatalogSkillFileContent("my-skill", ".git/config"),
|
|
685
|
+
).toBeNull();
|
|
686
|
+
expect(
|
|
687
|
+
await readCatalogSkillFileContent("my-skill", "docs/.hidden/file.md"),
|
|
688
|
+
).toBeNull();
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
test("rejects SKIP_DIRS paths and returns null without reading disk", async () => {
|
|
692
|
+
const root = makeTempSkillsDir();
|
|
693
|
+
writeSkill(root, "my-skill", {
|
|
694
|
+
"SKILL.md": "ok",
|
|
695
|
+
"node_modules/foo/index.js": "module.exports = {};",
|
|
696
|
+
});
|
|
697
|
+
mockRepoSkillsDir = root;
|
|
698
|
+
mockCatalog = [skill("my-skill")];
|
|
699
|
+
installFetchForbidden();
|
|
700
|
+
|
|
701
|
+
expect(
|
|
702
|
+
await readCatalogSkillFileContent(
|
|
703
|
+
"my-skill",
|
|
704
|
+
"node_modules/foo/index.js",
|
|
705
|
+
),
|
|
706
|
+
).toBeNull();
|
|
707
|
+
expect(
|
|
708
|
+
await readCatalogSkillFileContent("my-skill", "__pycache__/cached.pyc"),
|
|
709
|
+
).toBeNull();
|
|
710
|
+
expect(
|
|
711
|
+
await readCatalogSkillFileContent(
|
|
712
|
+
"my-skill",
|
|
713
|
+
"nested/node_modules/foo.js",
|
|
714
|
+
),
|
|
715
|
+
).toBeNull();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
test("regular docs/readme.md still returns content (sanity)", async () => {
|
|
719
|
+
const root = makeTempSkillsDir();
|
|
720
|
+
writeSkill(root, "my-skill", {
|
|
721
|
+
"docs/readme.md": "# readme\n",
|
|
722
|
+
});
|
|
723
|
+
mockRepoSkillsDir = root;
|
|
724
|
+
mockCatalog = [skill("my-skill")];
|
|
725
|
+
installFetchForbidden();
|
|
726
|
+
|
|
727
|
+
const entry = await readCatalogSkillFileContent(
|
|
728
|
+
"my-skill",
|
|
729
|
+
"docs/readme.md",
|
|
730
|
+
);
|
|
731
|
+
expect(entry).not.toBeNull();
|
|
732
|
+
expect(entry!.content).toBe("# readme\n");
|
|
733
|
+
expect(entry!.name).toBe("readme.md");
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
// ---------------------------------------------------------------------------
|
|
738
|
+
// readCatalogSkillFileContent — platform mode
|
|
739
|
+
// ---------------------------------------------------------------------------
|
|
740
|
+
|
|
741
|
+
describe("readCatalogSkillFileContent (platform mode)", () => {
|
|
742
|
+
test("maps snake_case text response to camelCase entry", async () => {
|
|
743
|
+
mockCatalog = [skill("remote-skill")];
|
|
744
|
+
installFetchMock(() =>
|
|
745
|
+
Response.json({
|
|
746
|
+
path: "SKILL.md",
|
|
747
|
+
name: "SKILL.md",
|
|
748
|
+
size: 14,
|
|
749
|
+
mime_type: "text/markdown",
|
|
750
|
+
is_binary: false,
|
|
751
|
+
content: "# hello world\n",
|
|
752
|
+
}),
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
const entry = await readCatalogSkillFileContent("remote-skill", "SKILL.md");
|
|
756
|
+
expect(entry).not.toBeNull();
|
|
757
|
+
expect(entry!.path).toBe("SKILL.md");
|
|
758
|
+
expect(entry!.name).toBe("SKILL.md");
|
|
759
|
+
expect(entry!.size).toBe(14);
|
|
760
|
+
expect(entry!.mimeType).toBe("text/markdown");
|
|
761
|
+
expect(entry!.isBinary).toBe(false);
|
|
762
|
+
expect(entry!.content).toBe("# hello world\n");
|
|
763
|
+
|
|
764
|
+
expect(fetchCalls.length).toBe(1);
|
|
765
|
+
const url = fetchCalls[0]!.url;
|
|
766
|
+
expect(
|
|
767
|
+
url.startsWith(
|
|
768
|
+
"https://platform.test/v1/skills/remote-skill/files/content/",
|
|
769
|
+
),
|
|
770
|
+
).toBe(true);
|
|
771
|
+
expect(url).toContain("path=SKILL.md");
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test("preserves binary response (content=null, isBinary=true)", async () => {
|
|
775
|
+
mockCatalog = [skill("remote-skill")];
|
|
776
|
+
installFetchMock(() =>
|
|
777
|
+
Response.json({
|
|
778
|
+
path: "img.png",
|
|
779
|
+
name: "img.png",
|
|
780
|
+
size: 1024,
|
|
781
|
+
mime_type: "image/png",
|
|
782
|
+
is_binary: true,
|
|
783
|
+
content: null,
|
|
784
|
+
}),
|
|
785
|
+
);
|
|
786
|
+
|
|
787
|
+
const entry = await readCatalogSkillFileContent("remote-skill", "img.png");
|
|
788
|
+
expect(entry).not.toBeNull();
|
|
789
|
+
expect(entry!.isBinary).toBe(true);
|
|
790
|
+
expect(entry!.content).toBeNull();
|
|
791
|
+
expect(entry!.mimeType).toBe("image/png");
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("preserves oversized text response (content=null)", async () => {
|
|
795
|
+
mockCatalog = [skill("remote-skill")];
|
|
796
|
+
installFetchMock(() =>
|
|
797
|
+
Response.json({
|
|
798
|
+
path: "big.txt",
|
|
799
|
+
name: "big.txt",
|
|
800
|
+
size: 3 * 1024 * 1024,
|
|
801
|
+
mime_type: "text/plain",
|
|
802
|
+
is_binary: false,
|
|
803
|
+
content: null,
|
|
804
|
+
}),
|
|
805
|
+
);
|
|
806
|
+
|
|
807
|
+
const entry = await readCatalogSkillFileContent("remote-skill", "big.txt");
|
|
808
|
+
expect(entry).not.toBeNull();
|
|
809
|
+
expect(entry!.isBinary).toBe(false);
|
|
810
|
+
expect(entry!.content).toBeNull();
|
|
811
|
+
expect(entry!.size).toBe(3 * 1024 * 1024);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
test("returns null on 404", async () => {
|
|
815
|
+
mockCatalog = [skill("remote-skill")];
|
|
816
|
+
installFetchMock(
|
|
817
|
+
() => new Response("missing", { status: 404, statusText: "Not Found" }),
|
|
818
|
+
);
|
|
819
|
+
expect(
|
|
820
|
+
await readCatalogSkillFileContent("remote-skill", "ghost.md"),
|
|
821
|
+
).toBeNull();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("rejects traversal BEFORE making any fetch call", async () => {
|
|
825
|
+
mockCatalog = [skill("remote-skill")];
|
|
826
|
+
installFetchForbidden();
|
|
827
|
+
expect(await readCatalogSkillFileContent("remote-skill", "..")).toBeNull();
|
|
828
|
+
expect(
|
|
829
|
+
await readCatalogSkillFileContent("remote-skill", "../etc/passwd"),
|
|
830
|
+
).toBeNull();
|
|
831
|
+
expect(fetchCalls.length).toBe(0);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
test("rejects hidden / SKIP_DIRS paths BEFORE making any fetch call", async () => {
|
|
835
|
+
// Platform-mode defense in depth: even though the platform endpoint
|
|
836
|
+
// would refuse these reads server-side, we must short-circuit in
|
|
837
|
+
// `readCatalogSkillFileContent` so an attacker cannot use the daemon
|
|
838
|
+
// as a probe channel (and so we avoid unnecessary network traffic).
|
|
839
|
+
mockCatalog = [skill("remote-skill")];
|
|
840
|
+
installFetchForbidden();
|
|
841
|
+
expect(
|
|
842
|
+
await readCatalogSkillFileContent("remote-skill", ".env"),
|
|
843
|
+
).toBeNull();
|
|
844
|
+
expect(
|
|
845
|
+
await readCatalogSkillFileContent("remote-skill", ".git/config"),
|
|
846
|
+
).toBeNull();
|
|
847
|
+
expect(
|
|
848
|
+
await readCatalogSkillFileContent(
|
|
849
|
+
"remote-skill",
|
|
850
|
+
"node_modules/foo/index.js",
|
|
851
|
+
),
|
|
852
|
+
).toBeNull();
|
|
853
|
+
expect(fetchCalls.length).toBe(0);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
test("returns null without fetching when skill id missing from catalog", async () => {
|
|
857
|
+
mockCatalog = [];
|
|
858
|
+
installFetchForbidden();
|
|
859
|
+
expect(await readCatalogSkillFileContent("unknown", "SKILL.md")).toBeNull();
|
|
860
|
+
expect(fetchCalls.length).toBe(0);
|
|
861
|
+
});
|
|
862
|
+
});
|