6060from ..evaluation .eval_result import EvalSetResult
6161from ..evaluation .eval_set import EvalSet
6262from .api_server import ApiServer
63+
64+ NESTED_APP_SEPARATOR = "."
6365from .utils import common
6466from .utils import evals
6567from .utils .graph_serialization import serialize_app_info
@@ -157,6 +159,40 @@ class DevServer(ApiServer):
157159 endpoints for evaluation, debugging, and developer UI features.
158160 """
159161
162+ def _get_agent_dir (self , app_name : str ) -> str :
163+ """Resolves the agent directory and validates the app name to prevent path traversal."""
164+ if not self .agents_dir :
165+ raise HTTPException (
166+ status_code = 500 , detail = "Agents directory is not configured"
167+ )
168+ if not app_name :
169+ raise HTTPException (status_code = 400 , detail = "App name cannot be empty" )
170+
171+ # Validate app_name structure (must be dot-separated identifiers)
172+ parts = app_name .split (NESTED_APP_SEPARATOR )
173+ for part in parts :
174+ if not part or not part .isidentifier ():
175+ raise HTTPException (
176+ status_code = 400 ,
177+ detail = (
178+ f"Invalid app name: { app_name !r} . App names must be valid "
179+ "Python identifiers or paths separated by dots."
180+ ),
181+ )
182+
183+ # Resolve path
184+ app_path = app_name .replace (NESTED_APP_SEPARATOR , "/" )
185+ agents_base = Path (self .agents_dir ).resolve ()
186+ resolved_path = (agents_base / app_path ).resolve ()
187+
188+ if not resolved_path .is_relative_to (agents_base ):
189+ raise HTTPException (
190+ status_code = 400 ,
191+ detail = f"Access denied: { app_name !r} is outside the agents directory" ,
192+ )
193+
194+ return str (resolved_path )
195+
160196 def _register_dev_endpoints (
161197 self ,
162198 app : FastAPI ,
@@ -502,7 +538,8 @@ async def get_app_info(app_name: str) -> Any:
502538 if self .agents_dir :
503539 import os
504540
505- readme_path = os .path .join (self .agents_dir , app_name , "README.md" )
541+ agent_dir = self ._get_agent_dir (app_name )
542+ readme_path = os .path .join (agent_dir , "README.md" )
506543 if os .path .exists (readme_path ):
507544 try :
508545 with open (readme_path , "r" , encoding = "utf-8" ) as f :
@@ -557,7 +594,7 @@ async def get_app_info_image(
557594 @app .get ("/dev/apps/{app_name}/tests" )
558595 async def list_tests (app_name : str ) -> list [str ]:
559596 """Lists all test JSON files for the given app."""
560- agent_dir = os . path . join ( self .agents_dir , app_name )
597+ agent_dir = self ._get_agent_dir ( app_name )
561598 tests_dir = os .path .join (agent_dir , "tests" )
562599 if not os .path .exists (tests_dir ):
563600 return []
@@ -573,7 +610,7 @@ async def rebuild_app_tests(
573610 app_name : str , test_name : Optional [str ] = None
574611 ) -> dict [str , str ]:
575612 """Rebuilds tests for the app."""
576- agent_dir = os . path . join ( self .agents_dir , app_name )
613+ agent_dir = self ._get_agent_dir ( app_name )
577614
578615 if test_name :
579616 if not test_name .endswith (".json" ):
@@ -592,7 +629,7 @@ async def run_app_tests(
592629 app_name : str , test_name : Optional [str ] = None
593630 ) -> StreamingResponse :
594631 """Runs tests and streams pytest output."""
595- agent_dir = os . path . join ( self .agents_dir , app_name )
632+ agent_dir = self ._get_agent_dir ( app_name )
596633
597634 import subprocess
598635 import sys
@@ -656,7 +693,7 @@ async def create_test(
656693 """Creates or updates a test file from session data."""
657694 # Sanitize test_name to prevent directory traversal
658695 test_name = os .path .basename (test_name )
659- agent_dir = os . path . join ( self .agents_dir , app_name )
696+ agent_dir = self ._get_agent_dir ( app_name )
660697 tests_dir = os .path .join (agent_dir , "tests" )
661698 os .makedirs (tests_dir , exist_ok = True )
662699
@@ -673,7 +710,7 @@ async def create_test(
673710 @app .delete ("/dev/apps/{app_name}/tests/{test_name}" )
674711 async def delete_test (app_name : str , test_name : str ) -> dict [str , str ]:
675712 """Deletes a specific test file."""
676- agent_dir = os . path . join ( self .agents_dir , app_name )
713+ agent_dir = self ._get_agent_dir ( app_name )
677714 tests_dir = os .path .join (agent_dir , "tests" )
678715
679716 if not test_name .endswith (".json" ):
@@ -690,7 +727,7 @@ async def delete_test(app_name: str, test_name: str) -> dict[str, str]:
690727 @app .get ("/dev/apps/{app_name}/tests/{test_name}" )
691728 async def get_test_content (app_name : str , test_name : str ) -> dict [str , Any ]:
692729 """Fetches the content of a specific test file."""
693- agent_dir = os . path . join ( self .agents_dir , app_name )
730+ agent_dir = self ._get_agent_dir ( app_name )
694731 tests_dir = os .path .join (agent_dir , "tests" )
695732
696733 if not test_name .endswith (".json" ):
0 commit comments