From 3ac7cde434c5529042dfdff3ecb8b679ab392967 Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:39:35 -0700 Subject: [PATCH] feat: add path parameter validation for source_id (#5524) 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 | 72 ++++++++++++++++++--- letta/server/rest_api/routers/v1/agents.py | 4 +- letta/server/rest_api/routers/v1/sources.py | 14 ++-- 3 files changed, 72 insertions(+), 18 deletions(-) diff --git a/fern/openapi.json b/fern/openapi.json index 9679c029..3b06afed 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -1704,8 +1704,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-'" } ], "responses": { @@ -1744,8 +1750,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-'" } ], "requestBody": { @@ -1794,8 +1806,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-'" } ], "responses": { @@ -1996,8 +2014,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": "duplicate_handling", @@ -2077,8 +2101,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-'" } ], "responses": { @@ -2123,8 +2153,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": "after", @@ -2217,8 +2253,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": "limit", @@ -4527,8 +4569,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": "agent_id", @@ -4645,8 +4693,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": "agent_id", diff --git a/letta/server/rest_api/routers/v1/agents.py b/letta/server/rest_api/routers/v1/agents.py index f3c178e9..a185bc62 100644 --- a/letta/server/rest_api/routers/v1/agents.py +++ b/letta/server/rest_api/routers/v1/agents.py @@ -485,7 +485,7 @@ async def modify_approval( @router.patch("/{agent_id}/sources/attach/{source_id}", response_model=AgentState, operation_id="attach_source_to_agent") async def attach_source( - source_id: str, + source_id: str = PATH_VALIDATORS["source"], agent_id: str = PATH_VALIDATORS["agent"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -539,7 +539,7 @@ async def attach_folder_to_agent( @router.patch("/{agent_id}/sources/detach/{source_id}", response_model=AgentState, operation_id="detach_source_from_agent") async def detach_source( - source_id: str, + source_id: str = PATH_VALIDATORS["source"], 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/sources.py b/letta/server/rest_api/routers/v1/sources.py index 71e5f94b..8091a2d3 100644 --- a/letta/server/rest_api/routers/v1/sources.py +++ b/letta/server/rest_api/routers/v1/sources.py @@ -60,7 +60,7 @@ async def count_sources( @router.get("/{source_id}", response_model=Source, operation_id="retrieve_source", deprecated=True) async def retrieve_source( - source_id: str, + source_id: str = PATH_VALIDATORS["source"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -157,8 +157,8 @@ async def create_source( @router.patch("/{source_id}", response_model=Source, operation_id="modify_source", deprecated=True) async def modify_source( - source_id: str, source: SourceUpdate, + source_id: str = PATH_VALIDATORS["source"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -173,7 +173,7 @@ async def modify_source( @router.delete("/{source_id}", response_model=None, operation_id="delete_source", deprecated=True) async def delete_source( - source_id: str, + source_id: str = PATH_VALIDATORS["source"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -209,7 +209,7 @@ async def delete_source( @router.post("/{source_id}/upload", response_model=FileMetadata, operation_id="upload_file_to_source", deprecated=True) async def upload_file_to_source( file: UploadFile, - source_id: str, + source_id: str = PATH_VALIDATORS["source"], duplicate_handling: DuplicateFileHandling = Query(DuplicateFileHandling.SUFFIX, description="How to handle duplicate filenames"), name: Optional[str] = Query(None, description="Optional custom name to override the uploaded file's name"), server: "SyncServer" = Depends(get_letta_server), @@ -324,7 +324,7 @@ async def upload_file_to_source( @router.get("/{source_id}/agents", response_model=List[str], operation_id="get_agents_for_source", deprecated=True) async def get_agents_for_source( - source_id: str, + source_id: str = PATH_VALIDATORS["source"], server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -337,7 +337,7 @@ async def get_agents_for_source( @router.get("/{source_id}/passages", response_model=List[Passage], operation_id="list_source_passages", deprecated=True) async def list_source_passages( - source_id: str, + source_id: str = PATH_VALIDATORS["source"], after: Optional[str] = Query(None, description="Message after which to retrieve the returned messages."), before: Optional[str] = Query(None, description="Message before which to retrieve the returned messages."), limit: int = Query(100, description="Maximum number of messages to retrieve."), @@ -359,7 +359,7 @@ async def list_source_passages( @router.get("/{source_id}/files", response_model=List[FileMetadata], operation_id="list_source_files", deprecated=True) async def list_source_files( - source_id: str, + source_id: str = PATH_VALIDATORS["source"], limit: int = Query(1000, description="Number of files to return"), after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"), include_content: bool = Query(False, description="Whether to include full file content"),