From 47bf549dfcf9def542747b686288d0a35ca3c31e Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:39:17 -0700 Subject: [PATCH] feat: add path parameter validation for file_id (#5522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude --- fern/openapi.json | 64 ++++++++++++++++++--- letta/server/rest_api/routers/v1/agents.py | 4 +- letta/server/rest_api/routers/v1/folders.py | 5 +- letta/server/rest_api/routers/v1/sources.py | 9 +-- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/fern/openapi.json b/fern/openapi.json index bc67175e..28278989 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -2317,8 +2317,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Source Id" - } + }, + "description": "The ID of the source in the format 'source-'" }, { "name": "file_id", @@ -2326,8 +2332,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], "title": "File Id" - } + }, + "description": "The ID of the file in the format 'file-'" }, { "name": "include_content", @@ -2380,8 +2392,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^source-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the source in the format 'source-'", + "examples": ["source-123e4567-e89b-42d3-8456-426614174000"], "title": "Source Id" - } + }, + "description": "The ID of the source in the format 'source-'" }, { "name": "file_id", @@ -2389,8 +2407,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], "title": "File Id" - } + }, + "description": "The ID of the file in the format 'file-'" } ], "responses": { @@ -3311,8 +3335,14 @@ "required": true, "schema": { "type": "string", + "minLength": 43, + "maxLength": 43, + "pattern": "^folder-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the folder in the format 'folder-'", + "examples": ["folder-123e4567-e89b-42d3-8456-426614174000"], "title": "Folder Id" - } + }, + "description": "The ID of the folder in the format 'folder-'" }, { "name": "file_id", @@ -3320,8 +3350,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], "title": "File Id" - } + }, + "description": "The ID of the file in the format 'file-'" } ], "responses": { @@ -4724,8 +4760,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], "title": "File Id" - } + }, + "description": "The ID of the file in the format 'file-'" }, { "name": "agent_id", @@ -4784,8 +4826,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^file-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + "description": "The ID of the file in the format 'file-'", + "examples": ["file-123e4567-e89b-42d3-8456-426614174000"], "title": "File Id" - } + }, + "description": "The ID of the file in the format 'file-'" }, { "name": "agent_id", diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index c7c6a7a5..cc26faf6 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -616,7 +616,7 @@ async def close_all_open_files( @router.patch("/{agent_id}/files/{file_id}/open", response_model=List[str], operation_id="open_file") async def open_file( - file_id: str, + file_id: str = PATH_VALIDATORS["file"], agent_id: str = PATH_VALIDATORS["agent"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -665,7 +665,7 @@ async def open_file( @router.patch("/{agent_id}/files/{file_id}/close", response_model=None, operation_id="close_file") async def close_file( - file_id: str, + file_id: str = PATH_VALIDATORS["file"], agent_id: str = PATH_VALIDATORS["agent"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), diff --git a/letta/server/rest_api/routers/v1/folders.py b/letta/server/rest_api/routers/v1/folders.py index 6394dd40..149f3177 100644 --- a/letta/server/rest_api/routers/v1/folders.py +++ b/letta/server/rest_api/routers/v1/folders.py @@ -37,6 +37,7 @@ from letta.services.file_processor.parser.markitdown_parser import MarkitdownFil from letta.services.file_processor.parser.mistral_parser import MistralFileParser from letta.settings import settings from letta.utils import safe_create_file_processing_task, safe_create_task, sanitize_filename +from letta.validators import PATH_VALIDATORS logger = get_logger(__name__) @@ -496,8 +497,8 @@ async def list_folder_files( # it's still good practice to return a status indicating the success or failure of the deletion @router.delete("/{folder_id}/{file_id}", status_code=204, operation_id="delete_file_from_folder") async def delete_file_from_folder( - folder_id: str, - file_id: str, + folder_id: str = PATH_VALIDATORS["folder"], + file_id: str = PATH_VALIDATORS["file"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): diff --git a/letta/server/rest_api/routers/v1/sources.py b/letta/server/rest_api/routers/v1/sources.py index 5402a77a..71e5f94b 100644 --- a/letta/server/rest_api/routers/v1/sources.py +++ b/letta/server/rest_api/routers/v1/sources.py @@ -36,6 +36,7 @@ from letta.services.file_processor.parser.markitdown_parser import MarkitdownFil from letta.services.file_processor.parser.mistral_parser import MistralFileParser from letta.settings import settings from letta.utils import safe_create_file_processing_task, safe_create_task, sanitize_filename +from letta.validators import PATH_VALIDATORS logger = get_logger(__name__) @@ -386,8 +387,8 @@ async def list_source_files( @router.get("/{source_id}/files/{file_id}", response_model=FileMetadata, operation_id="get_file_metadata", deprecated=True) async def get_file_metadata( - source_id: str, - file_id: str, + source_id: str = PATH_VALIDATORS["source"], + file_id: str = PATH_VALIDATORS["file"], include_content: bool = Query(False, description="Whether to include full file content"), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -412,8 +413,8 @@ async def get_file_metadata( # it's still good practice to return a status indicating the success or failure of the deletion @router.delete("/{source_id}/{file_id}", status_code=204, operation_id="delete_file_from_source", deprecated=True) async def delete_file_from_source( - source_id: str, - file_id: str, + source_id: str = PATH_VALIDATORS["source"], + file_id: str = PATH_VALIDATORS["file"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ):