From c4379c3b8b0132107c702446f5bb192b50802fa8 Mon Sep 17 00:00:00 2001 From: Kian Jones <11655409+kianjones9@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:37:05 -0700 Subject: [PATCH] feat: add path validation for archive, provider, sandbox, step, and identity IDs (#5526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add validation for archive_id in archives.py - Add validation for provider_id in providers.py - Add validation for sandbox_config_id in sandbox_configs.py - Add validation for step_id in steps.py - Add validation for identity_id in identities.py - Update validators.py to include new primitives and remove mcp_server - Regenerate OpenAPI schema and SDK 🤖 Generated with Claude Code Co-authored-by: Claude --- fern/openapi.json | 128 +++++++++++++++--- letta/server/rest_api/routers/v1/archives.py | 3 +- .../server/rest_api/routers/v1/identities.py | 13 +- letta/server/rest_api/routers/v1/providers.py | 9 +- .../rest_api/routers/v1/sandbox_configs.py | 9 +- letta/server/rest_api/routers/v1/steps.py | 13 +- letta/validators.py | 18 ++- 7 files changed, 155 insertions(+), 38 deletions(-) diff --git a/fern/openapi.json b/fern/openapi.json index 6e036434..edf2d369 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -212,8 +212,14 @@ "required": true, "schema": { "type": "string", + "minLength": 44, + "maxLength": 44, + "pattern": "^archive-[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 archive in the format 'archive-'", + "examples": ["archive-123e4567-e89b-42d3-8456-426614174000"], "title": "Archive Id" - } + }, + "description": "The ID of the archive in the format 'archive-'" } ], "requestBody": { @@ -8150,8 +8156,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[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 identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], "title": "Identity Id" - } + }, + "description": "The ID of the identity in the format 'identity-'" } ], "responses": { @@ -8188,8 +8200,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[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 identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], "title": "Identity Id" - } + }, + "description": "The ID of the identity in the format 'identity-'" } ], "requestBody": { @@ -8237,8 +8255,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[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 identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], "title": "Identity Id" - } + }, + "description": "The ID of the identity in the format 'identity-'" } ], "responses": { @@ -8275,8 +8299,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[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 identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], "title": "Identity Id" - } + }, + "description": "The ID of the identity in the format 'identity-'" } ], "requestBody": { @@ -8328,8 +8358,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[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 identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], "title": "Identity Id" - } + }, + "description": "The ID of the identity in the format 'identity-'" }, { "name": "before", @@ -8454,8 +8490,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^identity-[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 identity in the format 'identity-'", + "examples": ["identity-123e4567-e89b-42d3-8456-426614174000"], "title": "Identity Id" - } + }, + "description": "The ID of the identity in the format 'identity-'" }, { "name": "before", @@ -10263,8 +10305,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^provider-[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 provider in the format 'provider-'", + "examples": ["provider-123e4567-e89b-42d3-8456-426614174000"], "title": "Provider Id" - } + }, + "description": "The ID of the provider in the format 'provider-'" } ], "responses": { @@ -10302,8 +10350,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^provider-[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 provider in the format 'provider-'", + "examples": ["provider-123e4567-e89b-42d3-8456-426614174000"], "title": "Provider Id" - } + }, + "description": "The ID of the provider in the format 'provider-'" } ], "requestBody": { @@ -10351,8 +10405,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^provider-[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 provider in the format 'provider-'", + "examples": ["provider-123e4567-e89b-42d3-8456-426614174000"], "title": "Provider Id" - } + }, + "description": "The ID of the provider in the format 'provider-'" } ], "responses": { @@ -10428,8 +10488,14 @@ "required": true, "schema": { "type": "string", + "minLength": 45, + "maxLength": 45, + "pattern": "^provider-[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 provider in the format 'provider-'", + "examples": ["provider-123e4567-e89b-42d3-8456-426614174000"], "title": "Provider Id" - } + }, + "description": "The ID of the provider in the format 'provider-'" } ], "responses": { @@ -11589,8 +11655,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[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 step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], "title": "Step Id" - } + }, + "description": "The ID of the step in the format 'step-'" } ], "responses": { @@ -11630,8 +11702,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[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 step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], "title": "Step Id" - } + }, + "description": "The ID of the step in the format 'step-'" } ], "responses": { @@ -11670,8 +11748,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[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 step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], "title": "Step Id" - } + }, + "description": "The ID of the step in the format 'step-'" } ], "responses": { @@ -11719,8 +11803,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[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 step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], "title": "Step Id" - } + }, + "description": "The ID of the step in the format 'step-'" } ], "requestBody": { @@ -11770,8 +11860,14 @@ "required": true, "schema": { "type": "string", + "minLength": 41, + "maxLength": 41, + "pattern": "^step-[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 step in the format 'step-'", + "examples": ["step-123e4567-e89b-42d3-8456-426614174000"], "title": "Step Id" - } + }, + "description": "The ID of the step in the format 'step-'" }, { "name": "before", diff --git a/letta/server/rest_api/routers/v1/archives.py b/letta/server/rest_api/routers/v1/archives.py index d2790da6..2cb24917 100644 --- a/letta/server/rest_api/routers/v1/archives.py +++ b/letta/server/rest_api/routers/v1/archives.py @@ -6,6 +6,7 @@ from pydantic import BaseModel from letta.schemas.archive import Archive as PydanticArchive from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server from letta.server.server import SyncServer +from letta.validators import PATH_VALIDATORS router = APIRouter(prefix="/archives", tags=["archives"]) @@ -84,8 +85,8 @@ async def list_archives( @router.patch("/{archive_id}", response_model=PydanticArchive, operation_id="modify_archive") async def modify_archive( - archive_id: str, archive: ArchiveUpdateRequest = Body(...), + archive_id: str = PATH_VALIDATORS["archive"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): diff --git a/letta/server/rest_api/routers/v1/identities.py b/letta/server/rest_api/routers/v1/identities.py index 76a28fb3..af861403 100644 --- a/letta/server/rest_api/routers/v1/identities.py +++ b/letta/server/rest_api/routers/v1/identities.py @@ -7,6 +7,7 @@ from letta.schemas.agent import AgentState from letta.schemas.block import Block from letta.schemas.identity import Identity, IdentityCreate, IdentityProperty, IdentityType, IdentityUpdate, IdentityUpsert from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server +from letta.validators import PATH_VALIDATORS if TYPE_CHECKING: from letta.server.server import SyncServer @@ -72,7 +73,7 @@ async def count_identities( @router.get("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="retrieve_identity") async def retrieve_identity( - identity_id: str, + identity_id: str = PATH_VALIDATORS["identity"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -108,7 +109,7 @@ async def upsert_identity( @router.patch("/{identity_id}", tags=["identities"], response_model=Identity, operation_id="update_identity") async def modify_identity( - identity_id: str, + identity_id: str = PATH_VALIDATORS["identity"], identity: IdentityUpdate = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -119,7 +120,7 @@ async def modify_identity( @router.put("/{identity_id}/properties", tags=["identities"], operation_id="upsert_identity_properties") async def upsert_identity_properties( - identity_id: str, + identity_id: str = PATH_VALIDATORS["identity"], properties: List[IdentityProperty] = Body(...), server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), @@ -130,7 +131,7 @@ async def upsert_identity_properties( @router.delete("/{identity_id}", tags=["identities"], operation_id="delete_identity") async def delete_identity( - identity_id: str, + identity_id: str = PATH_VALIDATORS["identity"], server: "SyncServer" = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -143,7 +144,7 @@ async def delete_identity( @router.get("/{identity_id}/agents", response_model=List[AgentState], operation_id="list_agents_for_identity") async def list_agents_for_identity( - identity_id: str, + identity_id: str = PATH_VALIDATORS["identity"], before: Optional[str] = Query( None, description="Agent ID cursor for pagination. Returns agents that come before this agent ID in the specified sort order", @@ -176,7 +177,7 @@ async def list_agents_for_identity( @router.get("/{identity_id}/blocks", response_model=List[Block], operation_id="list_blocks_for_identity") async def list_blocks_for_identity( - identity_id: str, + identity_id: str = PATH_VALIDATORS["identity"], before: Optional[str] = Query( None, description="Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order", diff --git a/letta/server/rest_api/routers/v1/providers.py b/letta/server/rest_api/routers/v1/providers.py index 92ee13e2..933abc20 100644 --- a/letta/server/rest_api/routers/v1/providers.py +++ b/letta/server/rest_api/routers/v1/providers.py @@ -6,6 +6,7 @@ from fastapi.responses import JSONResponse from letta.schemas.enums import ProviderType from letta.schemas.providers import Provider, ProviderCheck, ProviderCreate, ProviderUpdate from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server +from letta.validators import PATH_VALIDATORS if TYPE_CHECKING: from letta.server.server import SyncServer @@ -45,7 +46,7 @@ async def list_providers( @router.get("/{provider_id}", response_model=Provider, operation_id="retrieve_provider") async def retrieve_provider( - provider_id: str, + provider_id: str = PATH_VALIDATORS["provider"], headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): @@ -79,8 +80,8 @@ async def create_provider( @router.patch("/{provider_id}", response_model=Provider, operation_id="modify_provider") async def modify_provider( - provider_id: str, request: ProviderUpdate = Body(...), + provider_id: str = PATH_VALIDATORS["provider"], headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): @@ -110,7 +111,7 @@ async def check_provider( @router.post("/{provider_id}/check", response_model=None, operation_id="check_existing_provider") async def check_existing_provider( - provider_id: str, + provider_id: str = PATH_VALIDATORS["provider"], headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): @@ -135,7 +136,7 @@ async def check_existing_provider( @router.delete("/{provider_id}", response_model=None, operation_id="delete_provider") async def delete_provider( - provider_id: str, + provider_id: str = PATH_VALIDATORS["provider"], headers: HeaderParams = Depends(get_headers), server: "SyncServer" = Depends(get_letta_server), ): diff --git a/letta/server/rest_api/routers/v1/sandbox_configs.py b/letta/server/rest_api/routers/v1/sandbox_configs.py index 94365ec3..4356c2dd 100644 --- a/letta/server/rest_api/routers/v1/sandbox_configs.py +++ b/letta/server/rest_api/routers/v1/sandbox_configs.py @@ -21,6 +21,7 @@ from letta.schemas.sandbox_config import ( from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server from letta.server.server import SyncServer from letta.services.helpers.tool_execution_helper import create_venv_for_local_sandbox, install_pip_requirements_for_sandbox +from letta.validators import PATH_VALIDATORS router = APIRouter(prefix="/sandbox-config", tags=["sandbox-config"]) @@ -87,8 +88,8 @@ async def create_custom_local_sandbox_config( @router.patch("/{sandbox_config_id}", response_model=PydanticSandboxConfig) async def update_sandbox_config( - sandbox_config_id: str, config_update: SandboxConfigUpdate, + sandbox_config_id: str = PATH_VALIDATORS["sandbox"], server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -98,7 +99,7 @@ async def update_sandbox_config( @router.delete("/{sandbox_config_id}", status_code=204) async def delete_sandbox_config( - sandbox_config_id: str, + sandbox_config_id: str = PATH_VALIDATORS["sandbox"], server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -157,8 +158,8 @@ async def force_recreate_local_sandbox_venv( @router.post("/{sandbox_config_id}/environment-variable", response_model=PydanticEnvVar) async def create_sandbox_env_var( - sandbox_config_id: str, env_var_create: SandboxEnvironmentVariableCreate, + sandbox_config_id: str = PATH_VALIDATORS["sandbox"], server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -189,7 +190,7 @@ async def delete_sandbox_env_var( @router.get("/{sandbox_config_id}/environment-variable", response_model=List[PydanticEnvVar]) async def list_sandbox_env_vars( - sandbox_config_id: str, + sandbox_config_id: str = PATH_VALIDATORS["sandbox"], limit: int = Query(1000, description="Number of results to return"), after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"), server: SyncServer = Depends(get_letta_server), diff --git a/letta/server/rest_api/routers/v1/steps.py b/letta/server/rest_api/routers/v1/steps.py index 8f29c951..2f2087f8 100644 --- a/letta/server/rest_api/routers/v1/steps.py +++ b/letta/server/rest_api/routers/v1/steps.py @@ -13,6 +13,7 @@ from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_le from letta.server.server import SyncServer from letta.services.step_manager import FeedbackType from letta.settings import settings +from letta.validators import PATH_VALIDATORS router = APIRouter(prefix="/steps", tags=["steps"]) @@ -69,7 +70,7 @@ async def list_steps( @router.get("/{step_id}", response_model=Step, operation_id="retrieve_step") async def retrieve_step( - step_id: str, + step_id: str = PATH_VALIDATORS["step"], headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), ): @@ -82,7 +83,7 @@ async def retrieve_step( @router.get("/{step_id}/metrics", response_model=StepMetrics, operation_id="retrieve_metrics_for_step") async def retrieve_metrics_for_step( - step_id: str, + step_id: str = PATH_VALIDATORS["step"], headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), ): @@ -95,7 +96,7 @@ async def retrieve_metrics_for_step( @router.get("/{step_id}/trace", response_model=Optional[ProviderTrace], operation_id="retrieve_trace_for_step") async def retrieve_trace_for_step( - step_id: str, + step_id: str = PATH_VALIDATORS["step"], server: SyncServer = Depends(get_letta_server), headers: HeaderParams = Depends(get_headers), ): @@ -118,8 +119,8 @@ class ModifyFeedbackRequest(BaseModel): @router.patch("/{step_id}/feedback", response_model=Step, operation_id="modify_feedback_for_step") async def modify_feedback_for_step( - step_id: str, request: ModifyFeedbackRequest = Body(...), + step_id: str = PATH_VALIDATORS["step"], headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), ): @@ -132,7 +133,7 @@ async def modify_feedback_for_step( @router.get("/{step_id}/messages", response_model=List[LettaMessageUnion], operation_id="list_messages_for_step") async def list_messages_for_step( - step_id: str, + step_id: str = PATH_VALIDATORS["step"], headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), before: Optional[str] = Query( @@ -159,8 +160,8 @@ async def list_messages_for_step( @router.patch("/{step_id}/transaction/{transaction_id}", response_model=Step, operation_id="update_step_transaction_id") async def update_step_transaction_id( - step_id: str, transaction_id: str, + step_id: str = PATH_VALIDATORS["step"], headers: HeaderParams = Depends(get_headers), server: SyncServer = Depends(get_letta_server), ): diff --git a/letta/validators.py b/letta/validators.py index 345310f9..a711693d 100644 --- a/letta/validators.py +++ b/letta/validators.py @@ -3,7 +3,23 @@ import re from fastapi import Path # TODO: extract this list from routers/v1/__init__.py and ROUTERS -primitives = ["agent", "message", "run", "job", "group", "block", "file", "folder", "source", "tool", "mcp_server"] +primitives = [ + "agent", + "message", + "run", + "job", + "group", + "block", + "file", + "folder", + "source", + "tool", + "archive", + "provider", + "sandbox", + "step", + "identity", +] PRIMITIVE_ID_PATTERNS = { # f-string interpolation gets confused because of the regex's required curly braces {}