diff --git a/api/.env.example b/api/.env.example index 683042a70ac..11be68e43d7 100644 --- a/api/.env.example +++ b/api/.env.example @@ -436,6 +436,7 @@ UPLOAD_FILE_BATCH_LIMIT=5 UPLOAD_IMAGE_FILE_SIZE_LIMIT=10 UPLOAD_VIDEO_FILE_SIZE_LIMIT=100 UPLOAD_AUDIO_FILE_SIZE_LIMIT=50 +UPLOAD_SKILL_FILE_SIZE_LIMIT=50 # Comma-separated list of file extensions blocked from upload for security reasons. # Extensions should be lowercase without dots (e.g., exe,bat,sh,dll). diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index 223d22082db..93b96689cc5 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -470,6 +470,11 @@ class FileUploadConfig(BaseSettings): default=50, ) + UPLOAD_SKILL_FILE_SIZE_LIMIT: NonNegativeInt = Field( + description="Maximum allowed Skill package size for uploads in megabytes", + default=50, + ) + BATCH_UPLOAD_LIMIT: NonNegativeInt = Field( description="Maximum number of files allowed in a batch upload operation", default=20, diff --git a/api/controllers/console/files.py b/api/controllers/console/files.py index a2df1a07845..110f4ad0a4b 100644 --- a/api/controllers/console/files.py +++ b/api/controllers/console/files.py @@ -107,6 +107,7 @@ class FileApi(Resource): image_file_size_limit=dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT, video_file_size_limit=dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT, audio_file_size_limit=dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT, + skill_file_size_limit=dify_config.UPLOAD_SKILL_FILE_SIZE_LIMIT, workflow_file_upload_limit=dify_config.WORKFLOW_FILE_UPLOAD_LIMIT, image_file_batch_limit=dify_config.IMAGE_FILE_BATCH_LIMIT, single_chunk_attachment_limit=dify_config.SINGLE_CHUNK_ATTACHMENT_LIMIT, diff --git a/api/fields/file_fields.py b/api/fields/file_fields.py index 681dc3db2d2..480c165a362 100644 --- a/api/fields/file_fields.py +++ b/api/fields/file_fields.py @@ -15,6 +15,7 @@ class UploadConfig(ResponseModel): image_file_size_limit: int video_file_size_limit: int audio_file_size_limit: int + skill_file_size_limit: int workflow_file_upload_limit: int image_file_batch_limit: int single_chunk_attachment_limit: int diff --git a/api/openapi/markdown/console-openapi.md b/api/openapi/markdown/console-openapi.md index 681dfa266ee..b2b61e4bec3 100644 --- a/api/openapi/markdown/console-openapi.md +++ b/api/openapi/markdown/console-openapi.md @@ -22989,6 +22989,7 @@ Payload for updating a snippet. | image_file_batch_limit | integer | | Yes | | image_file_size_limit | integer | | Yes | | single_chunk_attachment_limit | integer | | Yes | +| skill_file_size_limit | integer | | Yes | | video_file_size_limit | integer | | Yes | | workflow_file_upload_limit | integer | | Yes | diff --git a/api/services/agent/skill_package_service.py b/api/services/agent/skill_package_service.py index d5601987abc..fbfd2ababfc 100644 --- a/api/services/agent/skill_package_service.py +++ b/api/services/agent/skill_package_service.py @@ -26,8 +26,9 @@ import zlib import yaml from pydantic import BaseModel +from configs import dify_config + # Bounds — generous but finite so a hostile upload can't exhaust memory/disk. -_MAX_ARCHIVE_BYTES = 50 * 1024 * 1024 _MAX_UNCOMPRESSED_BYTES = 200 * 1024 * 1024 _MAX_SKILL_MD_BYTES = 1 * 1024 * 1024 _MAX_ENTRIES = 5000 @@ -127,7 +128,8 @@ class SkillPackageService: self._check_extension(filename) if not content: raise SkillPackageError("empty_archive", "skill archive is empty", status_code=400) - if len(content) > _MAX_ARCHIVE_BYTES: + max_archive_bytes = dify_config.UPLOAD_SKILL_FILE_SIZE_LIMIT * 1024 * 1024 + if len(content) > max_archive_bytes: raise SkillPackageError("archive_too_large", "skill archive exceeds size limit", status_code=400) try: diff --git a/api/tests/test_containers_integration_tests/controllers/console/test_files.py b/api/tests/test_containers_integration_tests/controllers/console/test_files.py index 8985c1ba66a..5e51b2ced98 100644 --- a/api/tests/test_containers_integration_tests/controllers/console/test_files.py +++ b/api/tests/test_containers_integration_tests/controllers/console/test_files.py @@ -37,6 +37,7 @@ def test_file_upload_config_returns_console_limits( "image_file_size_limit": dify_config.UPLOAD_IMAGE_FILE_SIZE_LIMIT, "video_file_size_limit": dify_config.UPLOAD_VIDEO_FILE_SIZE_LIMIT, "audio_file_size_limit": dify_config.UPLOAD_AUDIO_FILE_SIZE_LIMIT, + "skill_file_size_limit": dify_config.UPLOAD_SKILL_FILE_SIZE_LIMIT, "workflow_file_upload_limit": dify_config.WORKFLOW_FILE_UPLOAD_LIMIT, "image_file_batch_limit": dify_config.IMAGE_FILE_BATCH_LIMIT, "single_chunk_attachment_limit": dify_config.SINGLE_CHUNK_ATTACHMENT_LIMIT, diff --git a/api/tests/unit_tests/controllers/console/test_files.py b/api/tests/unit_tests/controllers/console/test_files.py index c36b6395291..cc407cd51b8 100644 --- a/api/tests/unit_tests/controllers/console/test_files.py +++ b/api/tests/unit_tests/controllers/console/test_files.py @@ -5,6 +5,7 @@ import pytest from flask import Flask from werkzeug.exceptions import Forbidden +from configs import dify_config from constants import DOCUMENT_EXTENSIONS from controllers.common.errors import ( BlockedFileExtensionError, @@ -92,6 +93,7 @@ class TestFileApiGet: assert status == 200 assert "file_size_limit" in data assert "batch_count_limit" in data + assert data["skill_file_size_limit"] == dify_config.UPLOAD_SKILL_FILE_SIZE_LIMIT class TestFileApiPost: diff --git a/api/tests/unit_tests/fields/test_file_fields.py b/api/tests/unit_tests/fields/test_file_fields.py index 2e33172aedd..28f3e2f6f9d 100644 --- a/api/tests/unit_tests/fields/test_file_fields.py +++ b/api/tests/unit_tests/fields/test_file_fields.py @@ -72,15 +72,17 @@ def test_remote_file_info_and_upload_config() -> None: image_file_size_limit=4, video_file_size_limit=5, audio_file_size_limit=6, - workflow_file_upload_limit=7, - image_file_batch_limit=8, - single_chunk_attachment_limit=9, - attachment_image_file_size_limit=10, + skill_file_size_limit=7, + workflow_file_upload_limit=8, + image_file_batch_limit=9, + single_chunk_attachment_limit=10, + attachment_image_file_size_limit=11, ) dumped = config.model_dump(mode="json") assert dumped["file_upload_limit"] == 3 - assert dumped["attachment_image_file_size_limit"] == 10 + assert dumped["skill_file_size_limit"] == 7 + assert dumped["attachment_image_file_size_limit"] == 11 @pytest.mark.parametrize( diff --git a/api/tests/unit_tests/services/agent/test_skill_package_service.py b/api/tests/unit_tests/services/agent/test_skill_package_service.py index 5c4e479e187..451069a3f9a 100644 --- a/api/tests/unit_tests/services/agent/test_skill_package_service.py +++ b/api/tests/unit_tests/services/agent/test_skill_package_service.py @@ -210,10 +210,10 @@ def test_validate_and_normalize_rejects_archive_too_large_uncompressed(monkeypat def test_validate_and_normalize_rejects_archive_too_large_uploaded_bytes(monkeypatch: pytest.MonkeyPatch): - monkeypatch.setattr(skill_package_service_module, "_MAX_ARCHIVE_BYTES", 8) + monkeypatch.setattr(skill_package_service_module.dify_config, "UPLOAD_SKILL_FILE_SIZE_LIMIT", 1) with pytest.raises(SkillPackageError) as exc_info: - SkillPackageService().validate_and_normalize(content=b"x" * 9, filename="skill.zip") + SkillPackageService().validate_and_normalize(content=b"x" * (1024 * 1024 + 1), filename="skill.zip") assert exc_info.value.code == "archive_too_large" diff --git a/docker/envs/core-services/shared.env.example b/docker/envs/core-services/shared.env.example index dbbe76c6a47..e68545d1a71 100644 --- a/docker/envs/core-services/shared.env.example +++ b/docker/envs/core-services/shared.env.example @@ -60,6 +60,7 @@ MULTIMODAL_SEND_FORMAT=base64 UPLOAD_IMAGE_FILE_SIZE_LIMIT=10 UPLOAD_VIDEO_FILE_SIZE_LIMIT=100 UPLOAD_AUDIO_FILE_SIZE_LIMIT=50 +UPLOAD_SKILL_FILE_SIZE_LIMIT=50 API_SENTRY_DSN= API_SENTRY_TRACES_SAMPLE_RATE=1.0 API_SENTRY_PROFILES_SAMPLE_RATE=1.0 diff --git a/packages/contracts/generated/api/console/files/types.gen.ts b/packages/contracts/generated/api/console/files/types.gen.ts index be26853f3bf..84ecb3af4af 100644 --- a/packages/contracts/generated/api/console/files/types.gen.ts +++ b/packages/contracts/generated/api/console/files/types.gen.ts @@ -17,6 +17,7 @@ export type UploadConfig = { image_file_batch_limit: number image_file_size_limit: number single_chunk_attachment_limit: number + skill_file_size_limit: number video_file_size_limit: number workflow_file_upload_limit: number } diff --git a/packages/contracts/generated/api/console/files/zod.gen.ts b/packages/contracts/generated/api/console/files/zod.gen.ts index d3d35b401a3..b10011a62e2 100644 --- a/packages/contracts/generated/api/console/files/zod.gen.ts +++ b/packages/contracts/generated/api/console/files/zod.gen.ts @@ -21,6 +21,7 @@ export const zUploadConfig = z.object({ image_file_batch_limit: z.int(), image_file_size_limit: z.int(), single_chunk_attachment_limit: z.int(), + skill_file_size_limit: z.int(), video_file_size_limit: z.int(), workflow_file_upload_limit: z.int(), })