@@ -306,8 +306,132 @@ def _has_parent_reference(path: str) -> bool:
306306 # functions, which is an RCE vector when exposed through the builder UI.
307307 # Block any upload that contains an `args` key anywhere in the document.
308308 _BLOCKED_YAML_KEYS = frozenset ({"args" })
309+ _BUILDER_BUILT_IN_AGENT_CLASSES = frozenset ({
310+ "LlmAgent" ,
311+ "LoopAgent" ,
312+ "ParallelAgent" ,
313+ "SequentialAgent" ,
314+ "google.adk.agents.LlmAgent" ,
315+ "google.adk.agents.LoopAgent" ,
316+ "google.adk.agents.ParallelAgent" ,
317+ "google.adk.agents.SequentialAgent" ,
318+ "google.adk.agents.llm_agent.LlmAgent" ,
319+ "google.adk.agents.loop_agent.LoopAgent" ,
320+ "google.adk.agents.parallel_agent.ParallelAgent" ,
321+ "google.adk.agents.sequential_agent.SequentialAgent" ,
322+ })
323+ _BUILDER_CODE_CONFIG_KEYS = frozenset ({
324+ "after_agent_callbacks" ,
325+ "after_model_callbacks" ,
326+ "after_tool_callbacks" ,
327+ "before_agent_callbacks" ,
328+ "before_model_callbacks" ,
329+ "before_tool_callbacks" ,
330+ "input_schema" ,
331+ "model_code" ,
332+ "output_schema" ,
333+ })
334+ _BUILDER_RESERVED_TOP_LEVEL_MODULES = frozenset ({"google" })
335+
336+ def _app_name_conflicts_with_importable_module (app_name : str ) -> bool :
337+ """Return whether app_name would make project references ambiguous."""
338+ stdlib_module_names = getattr (sys , "stdlib_module_names" , frozenset ())
339+ return (
340+ app_name in sys .builtin_module_names
341+ or app_name in stdlib_module_names
342+ or app_name in _BUILDER_RESERVED_TOP_LEVEL_MODULES
343+ )
344+
345+ def _check_project_code_reference (
346+ reference : str ,
347+ * ,
348+ app_name : str ,
349+ filename : str ,
350+ field_name : str ,
351+ allow_short_builtin_tool : bool = False ,
352+ ) -> None :
353+ """Validate that builder YAML cannot import arbitrary external code."""
354+ if "." not in reference :
355+ if allow_short_builtin_tool :
356+ return
357+ raise ValueError (
358+ f"Invalid code reference { reference !r} in { filename !r} . "
359+ f"The '{ field_name } ' field must use a project-local dotted path."
360+ )
361+
362+ if not reference .startswith (f"{ app_name } ." ):
363+ raise ValueError (
364+ f"Blocked code reference { reference !r} in { filename !r} . "
365+ f"The '{ field_name } ' field must reference code under "
366+ f"'{ app_name } .*'."
367+ )
368+
369+ if _app_name_conflicts_with_importable_module (app_name ):
370+ raise ValueError (
371+ f"Blocked code reference { reference !r} in { filename !r} . "
372+ f"The app name { app_name !r} conflicts with an importable Python "
373+ "module, so project-local code references would be ambiguous."
374+ )
309375
310- def _check_yaml_for_blocked_keys (content : bytes , filename : str ) -> None :
376+ def _check_agent_class_reference (
377+ value : Any , * , app_name : str , filename : str
378+ ) -> None :
379+ if not isinstance (value , str ):
380+ return
381+ if value in _BUILDER_BUILT_IN_AGENT_CLASSES :
382+ return
383+ _check_project_code_reference (
384+ value ,
385+ app_name = app_name ,
386+ filename = filename ,
387+ field_name = "agent_class" ,
388+ )
389+
390+ def _check_code_config_reference (
391+ value : Any , * , app_name : str , filename : str , field_name : str
392+ ) -> None :
393+ if isinstance (value , list ):
394+ for item in value :
395+ _check_code_config_reference (
396+ item ,
397+ app_name = app_name ,
398+ filename = filename ,
399+ field_name = field_name ,
400+ )
401+ return
402+ if not isinstance (value , dict ):
403+ return
404+
405+ name = value .get ("name" )
406+ if isinstance (name , str ):
407+ _check_project_code_reference (
408+ name ,
409+ app_name = app_name ,
410+ filename = filename ,
411+ field_name = field_name ,
412+ )
413+
414+ def _check_tool_references (
415+ value : Any , * , app_name : str , filename : str
416+ ) -> None :
417+ if not isinstance (value , list ):
418+ return
419+ for item in value :
420+ if not isinstance (item , dict ):
421+ continue
422+ name = item .get ("name" )
423+ if isinstance (name , str ):
424+ _check_project_code_reference (
425+ name ,
426+ app_name = app_name ,
427+ filename = filename ,
428+ field_name = "tools.name" ,
429+ allow_short_builtin_tool = True ,
430+ )
431+
432+ def _check_yaml_for_blocked_keys (
433+ content : bytes , filename : str , app_name : str
434+ ) -> None :
311435 """Raise if the YAML document contains any blocked keys."""
312436 import yaml
313437
@@ -325,6 +449,29 @@ def _walk(node: Any) -> None:
325449 f"The '{ key } ' field is not allowed in builder uploads "
326450 "because it can execute arbitrary code."
327451 )
452+ if key == "agent_class" :
453+ _check_agent_class_reference (
454+ value , app_name = app_name , filename = filename
455+ )
456+ elif key == "code" :
457+ if isinstance (value , str ):
458+ _check_project_code_reference (
459+ value ,
460+ app_name = app_name ,
461+ filename = filename ,
462+ field_name = "code" ,
463+ )
464+ elif key == "tools" :
465+ _check_tool_references (
466+ value , app_name = app_name , filename = filename
467+ )
468+ elif key in _BUILDER_CODE_CONFIG_KEYS :
469+ _check_code_config_reference (
470+ value ,
471+ app_name = app_name ,
472+ filename = filename ,
473+ field_name = key ,
474+ )
328475 _walk (value )
329476 elif isinstance (node , list ):
330477 for item in node :
@@ -490,7 +637,9 @@ async def builder_build(
490637
491638 # Phase 2: validate every file *before* writing anything to disk.
492639 for rel_path , content in uploads :
493- _check_yaml_for_blocked_keys (content , f"{ app_name } /{ rel_path } " )
640+ _check_yaml_for_blocked_keys (
641+ content , f"{ app_name } /{ rel_path } " , app_name
642+ )
494643
495644 # Phase 3: write validated files to disk.
496645 if tmp :
0 commit comments