@@ -1381,6 +1381,176 @@ public void testCallbackRefAccessors() {
13811381 assertThat (callbackRef .name ()).isEqualTo ("updated-name" );
13821382 }
13831383
1384+ @ Test
1385+ public void resolveSubAgents_withAbsoluteConfigPath_throwsConfigurationException ()
1386+ throws IOException {
1387+ // A secret file outside the agent directory that an attacker might try to read.
1388+ File secretFile = tempFolder .newFile ("secret.yaml" );
1389+ Files .writeString (
1390+ secretFile .toPath (),
1391+ """
1392+ agent_class: LlmAgent
1393+ name: secret_agent
1394+ description: A secret agent that should not be loadable via absolute path
1395+ instruction: secret instruction
1396+ """ );
1397+ String absoluteConfigPath = secretFile .getAbsolutePath ();
1398+
1399+ File mainAgentFile = tempFolder .newFile ("main_agent_absolute.yaml" );
1400+ Files .writeString (
1401+ mainAgentFile .toPath (),
1402+ String .format (
1403+ """
1404+ agent_class: LlmAgent
1405+ name: main_agent
1406+ description: Main agent referencing an absolute config_path
1407+ instruction: You are a main agent
1408+ sub_agents:
1409+ - name: secret_agent
1410+ config_path: %s
1411+ """ ,
1412+ absoluteConfigPath ));
1413+
1414+ ConfigurationException exception =
1415+ assertThrows (
1416+ ConfigurationException .class ,
1417+ () -> ConfigAgentUtils .fromConfig (mainAgentFile .getAbsolutePath ()));
1418+
1419+ assertThat (exception ).hasMessageThat ().contains ("Failed to create agent from config" );
1420+ StringBuilder messages = new StringBuilder ();
1421+ Throwable t = exception .getCause ();
1422+ while (t != null ) {
1423+ messages .append (t .getMessage ()).append ("\n " );
1424+ t = t .getCause ();
1425+ }
1426+ assertThat (messages .toString ())
1427+ .contains ("Absolute paths are not allowed in AgentTool config_path: " + absoluteConfigPath );
1428+ }
1429+
1430+ @ Test
1431+ public void resolveSubAgents_withTraversalConfigPath_throwsConfigurationException ()
1432+ throws IOException {
1433+ // The agent config lives in a nested subdirectory so that "../../" escapes the agent
1434+ // directory. The traversal target is a real file to prove the rejection happens before any
1435+ // file read.
1436+ File secretFile = tempFolder .newFile ("outside_secret.yaml" );
1437+ Files .writeString (
1438+ secretFile .toPath (),
1439+ """
1440+ agent_class: LlmAgent
1441+ name: outside_secret_agent
1442+ description: A file outside the agent directory
1443+ instruction: secret instruction
1444+ """ );
1445+
1446+ File agentDir = tempFolder .newFolder ("nested" , "agents" );
1447+ File mainAgentFile = new File (agentDir , "main_agent_traversal.yaml" );
1448+ Files .writeString (
1449+ mainAgentFile .toPath (),
1450+ """
1451+ agent_class: LlmAgent
1452+ name: main_agent
1453+ description: Main agent referencing a traversal config_path
1454+ instruction: You are a main agent
1455+ sub_agents:
1456+ - name: outside_secret_agent
1457+ config_path: ../../outside_secret.yaml
1458+ """ );
1459+
1460+ ConfigurationException exception =
1461+ assertThrows (
1462+ ConfigurationException .class ,
1463+ () -> ConfigAgentUtils .fromConfig (mainAgentFile .getAbsolutePath ()));
1464+
1465+ assertThat (exception ).hasMessageThat ().contains ("Failed to create agent from config" );
1466+ StringBuilder messages = new StringBuilder ();
1467+ Throwable t = exception .getCause ();
1468+ while (t != null ) {
1469+ messages .append (t .getMessage ()).append ("\n " );
1470+ t = t .getCause ();
1471+ }
1472+ assertThat (messages .toString ())
1473+ .contains (
1474+ "Path traversal detected: config_path resolves outside agent directory:"
1475+ + " ../../outside_secret.yaml" );
1476+ }
1477+
1478+ @ Test
1479+ public void resolveSubAgents_withRelativeConfigPathWithinAgentDir_resolvesSuccessfully ()
1480+ throws IOException , ConfigurationException {
1481+ // A normal relative config_path that stays within the agent directory must still work.
1482+ File subAgentFile = tempFolder .newFile ("normal_sub_agent.yaml" );
1483+ Files .writeString (
1484+ subAgentFile .toPath (),
1485+ """
1486+ agent_class: LlmAgent
1487+ name: normal_sub_agent
1488+ description: A normal subagent
1489+ instruction: You are a helpful subagent
1490+ """ );
1491+
1492+ File mainAgentFile = tempFolder .newFile ("main_agent_relative.yaml" );
1493+ Files .writeString (
1494+ mainAgentFile .toPath (),
1495+ """
1496+ agent_class: LlmAgent
1497+ name: main_agent
1498+ description: Main agent with a normal relative config_path
1499+ instruction: You are a main agent
1500+ sub_agents:
1501+ - name: normal_sub_agent
1502+ config_path: normal_sub_agent.yaml
1503+ """ );
1504+
1505+ BaseAgent mainAgent = ConfigAgentUtils .fromConfig (mainAgentFile .getAbsolutePath ());
1506+
1507+ assertThat (mainAgent .name ()).isEqualTo ("main_agent" );
1508+ assertThat (mainAgent .subAgents ()).hasSize (1 );
1509+ BaseAgent subAgent = mainAgent .subAgents ().get (0 );
1510+ assertThat (subAgent .name ()).isEqualTo ("normal_sub_agent" );
1511+ assertThat (subAgent ).isInstanceOf (LlmAgent .class );
1512+ }
1513+
1514+ @ Test
1515+ public void resolveSubAgents_withTraversalThatStaysWithinAgentDir_resolvesSuccessfully ()
1516+ throws IOException , ConfigurationException {
1517+ // A config_path containing ".." that still normalizes to a location inside the agent
1518+ // directory must be accepted (the containment check is on the normalized path, not a naive
1519+ // ".." substring match).
1520+ File childDir = tempFolder .newFolder ("child" );
1521+ File subAgentFile = new File (childDir , "nested_sub_agent.yaml" );
1522+ Files .writeString (
1523+ subAgentFile .toPath (),
1524+ """
1525+ agent_class: LlmAgent
1526+ name: nested_sub_agent
1527+ description: A nested subagent reached via an in-bounds relative path
1528+ instruction: You are a helpful subagent
1529+ """ );
1530+
1531+ File mainAgentFile = tempFolder .newFile ("main_agent_inbounds_traversal.yaml" );
1532+ // child/../child/nested_sub_agent.yaml normalizes back into the agent directory.
1533+ Files .writeString (
1534+ mainAgentFile .toPath (),
1535+ """
1536+ agent_class: LlmAgent
1537+ name: main_agent
1538+ description: Main agent with an in-bounds traversal config_path
1539+ instruction: You are a main agent
1540+ sub_agents:
1541+ - name: nested_sub_agent
1542+ config_path: child/../child/nested_sub_agent.yaml
1543+ """ );
1544+
1545+ BaseAgent mainAgent = ConfigAgentUtils .fromConfig (mainAgentFile .getAbsolutePath ());
1546+
1547+ assertThat (mainAgent .name ()).isEqualTo ("main_agent" );
1548+ assertThat (mainAgent .subAgents ()).hasSize (1 );
1549+ BaseAgent subAgent = mainAgent .subAgents ().get (0 );
1550+ assertThat (subAgent .name ()).isEqualTo ("nested_sub_agent" );
1551+ assertThat (subAgent ).isInstanceOf (LlmAgent .class );
1552+ }
1553+
13841554 @ Test
13851555 public void fromConfig_validYamlLoopAgent_createsLoopAgent ()
13861556 throws IOException , ConfigurationException {
0 commit comments