Skip to content

Commit b3da79a

Browse files
authored
fix(tool): built-in file tools support relative tools (#747)
1 parent f153fb0 commit b3da79a

File tree

4 files changed

+249
-15
lines changed

4 files changed

+249
-15
lines changed

agentscope-core/src/main/java/io/agentscope/core/skill/AgentSkillPromptProvider.java

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,23 @@ public class AgentSkillPromptProvider {
3737
## Available Skills
3838
3939
<usage>
40-
When you need to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities, tools, and domain knowledge.
40+
Skills provide specialized capabilities and domain knowledge. Use them when they match your current task.
4141
4242
How to use skills:
4343
- Load skill: load_skill_through_path(skillId="<skill-id>", path="SKILL.md")
4444
- The skill will be activated and its documentation loaded with detailed instructions
4545
- Additional resources (scripts, assets, references) can be loaded using the same tool with different paths
4646
47-
Usage notes:
48-
- Only use skills listed in <available_skills> below
49-
- Loading SKILL.md activates the skill and will make its tools available
50-
51-
Code execution guidance:
52-
- If a task requires running code, use the code execution tools (shell/read/write)
53-
when they are available
54-
- Code execution tools operate in workDir; skill scripts are uploaded to
55-
workDir/skills/<skill-id>/scripts (for example: workDir/skills/skill-ID/scripts/shell.sh)
56-
- Example (shell): execute_shell_command(command="skills/skill-ID/scripts/shell.sh")
47+
Path Information:
48+
When you load a skill, the response will include:
49+
- Exact paths to all skill resources
50+
- Code examples for accessing skill files
51+
- Usage instructions specific to that skill
5752
5853
Template fields explanation:
59-
- <name>: The skill's display name. Use it along with <description> to determine if this skill is relevant to your current task
60-
- <description>: Detailed description of when and how to use this skill. Read carefully to decide whether to load this skill
61-
- <skill-id>: The unique identifier used to load the skill via load_skill_through_path tool
54+
- <name>: The skill's display name
55+
- <description>: When and how to use this skill
56+
- <skill-id>: Unique identifier for load_skill_through_path tool
6257
</usage>
6358
6459
<available_skills>

agentscope-core/src/main/java/io/agentscope/core/tool/file/FileToolUtils.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ private FileToolUtils() {
3535
* Validate that the given file path is within the base directory.
3636
* This prevents path traversal attacks and unauthorized file access.
3737
*
38+
* <p>When baseDir is specified:
39+
* <ul>
40+
* <li>Relative paths are resolved relative to baseDir</li>
41+
* <li>Absolute paths are validated to be within baseDir</li>
42+
* </ul>
43+
*
3844
* @param filePath The file path to validate
3945
* @param baseDir The base directory to restrict access to (null means no restriction)
4046
* @return The normalized absolute path if valid
@@ -45,7 +51,17 @@ static Path validatePath(String filePath, Path baseDir) throws IOException {
4551
throw new IOException("File path cannot be null or empty.");
4652
}
4753

48-
Path path = Paths.get(filePath).toAbsolutePath().normalize();
54+
Path inputPath = Paths.get(filePath);
55+
56+
// If baseDir is specified, relative paths should be resolved relative to baseDir
57+
Path path;
58+
if (baseDir != null && !inputPath.isAbsolute()) {
59+
// Relative path: resolve relative to baseDir
60+
path = baseDir.resolve(inputPath).normalize();
61+
} else {
62+
// Absolute path or no baseDir: convert to absolute path
63+
path = inputPath.toAbsolutePath().normalize();
64+
}
4965

5066
// If baseDir is specified, ensure the path is within it
5167
if (baseDir != null) {

agentscope-core/src/test/java/io/agentscope/core/tool/file/FileToolUtilsTest.java

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,4 +251,165 @@ void testParseRanges_NullInput() {
251251
},
252252
"Should throw NullPointerException for null input");
253253
}
254+
255+
// ==================== validatePath Tests ====================
256+
257+
@Test
258+
@DisplayName("Should validate relative path with baseDir")
259+
void testValidatePath_RelativePathWithBaseDir() throws IOException {
260+
Path baseDir = tempDir;
261+
Path subDir = tempDir.resolve("subdir");
262+
Files.createDirectories(subDir);
263+
264+
// Relative path should be resolved relative to baseDir
265+
Path result = FileToolUtils.validatePath("subdir/test.txt", baseDir);
266+
267+
assertNotNull(result);
268+
assertTrue(result.isAbsolute(), "Result should be absolute");
269+
assertTrue(result.startsWith(baseDir), "Result should be within baseDir");
270+
assertEquals(subDir.resolve("test.txt"), result, "Should resolve to correct path");
271+
}
272+
273+
@Test
274+
@DisplayName("Should validate simple relative path with baseDir")
275+
void testValidatePath_SimpleRelativePathWithBaseDir() throws IOException {
276+
Path baseDir = tempDir;
277+
278+
Path result = FileToolUtils.validatePath("test.txt", baseDir);
279+
280+
assertNotNull(result);
281+
assertTrue(result.isAbsolute(), "Result should be absolute");
282+
assertTrue(result.startsWith(baseDir), "Result should be within baseDir");
283+
assertEquals(baseDir.resolve("test.txt"), result, "Should resolve to correct path");
284+
}
285+
286+
@Test
287+
@DisplayName("Should reject path traversal attack with baseDir")
288+
void testValidatePath_PathTraversalAttackWithBaseDir() {
289+
Path baseDir = tempDir;
290+
291+
// Try to escape baseDir using ../
292+
IOException exception =
293+
assertThrows(
294+
IOException.class,
295+
() -> FileToolUtils.validatePath("../../etc/passwd", baseDir),
296+
"Should reject path traversal attack");
297+
298+
assertTrue(
299+
exception.getMessage().contains("Access denied"),
300+
"Error message should indicate access denied");
301+
assertTrue(
302+
exception.getMessage().contains("outside the allowed base directory"),
303+
"Error message should mention base directory restriction");
304+
}
305+
306+
@Test
307+
@DisplayName("Should validate absolute path within baseDir")
308+
void testValidatePath_AbsolutePathWithinBaseDir() throws IOException {
309+
Path baseDir = tempDir;
310+
Path targetFile = tempDir.resolve("test.txt");
311+
312+
Path result = FileToolUtils.validatePath(targetFile.toString(), baseDir);
313+
314+
assertNotNull(result);
315+
assertTrue(result.isAbsolute(), "Result should be absolute");
316+
assertEquals(targetFile, result, "Should return the same absolute path");
317+
}
318+
319+
@Test
320+
@DisplayName("Should reject absolute path outside baseDir")
321+
void testValidatePath_AbsolutePathOutsideBaseDir() {
322+
Path baseDir = tempDir;
323+
Path outsidePath = tempDir.getParent().resolve("outside.txt");
324+
325+
IOException exception =
326+
assertThrows(
327+
IOException.class,
328+
() -> FileToolUtils.validatePath(outsidePath.toString(), baseDir),
329+
"Should reject path outside baseDir");
330+
331+
assertTrue(
332+
exception.getMessage().contains("Access denied"),
333+
"Error message should indicate access denied");
334+
}
335+
336+
@Test
337+
@DisplayName("Should validate relative path without baseDir")
338+
void testValidatePath_RelativePathWithoutBaseDir() throws IOException {
339+
// Without baseDir, relative path should be converted to absolute
340+
Path result = FileToolUtils.validatePath("test.txt", null);
341+
342+
assertNotNull(result);
343+
assertTrue(result.isAbsolute(), "Result should be absolute");
344+
assertTrue(result.toString().endsWith("test.txt"), "Result should end with the file name");
345+
}
346+
347+
@Test
348+
@DisplayName("Should validate absolute path without baseDir")
349+
void testValidatePath_AbsolutePathWithoutBaseDir() throws IOException {
350+
Path absolutePath = tempDir.resolve("test.txt");
351+
352+
Path result = FileToolUtils.validatePath(absolutePath.toString(), null);
353+
354+
assertNotNull(result);
355+
assertTrue(result.isAbsolute(), "Result should be absolute");
356+
assertEquals(absolutePath, result, "Should return the same absolute path");
357+
}
358+
359+
@Test
360+
@DisplayName("Should reject null file path")
361+
void testValidatePath_NullFilePath() {
362+
IOException exception =
363+
assertThrows(
364+
IOException.class,
365+
() -> FileToolUtils.validatePath(null, tempDir),
366+
"Should reject null file path");
367+
368+
assertTrue(
369+
exception.getMessage().contains("cannot be null or empty"),
370+
"Error message should mention null or empty");
371+
}
372+
373+
@Test
374+
@DisplayName("Should reject empty file path")
375+
void testValidatePath_EmptyFilePath() {
376+
IOException exception =
377+
assertThrows(
378+
IOException.class,
379+
() -> FileToolUtils.validatePath("", tempDir),
380+
"Should reject empty file path");
381+
382+
assertTrue(
383+
exception.getMessage().contains("cannot be null or empty"),
384+
"Error message should mention null or empty");
385+
}
386+
387+
@Test
388+
@DisplayName("Should reject whitespace-only file path")
389+
void testValidatePath_WhitespaceFilePath() {
390+
IOException exception =
391+
assertThrows(
392+
IOException.class,
393+
() -> FileToolUtils.validatePath(" ", tempDir),
394+
"Should reject whitespace-only file path");
395+
396+
assertTrue(
397+
exception.getMessage().contains("cannot be null or empty"),
398+
"Error message should mention null or empty");
399+
}
400+
401+
@Test
402+
@DisplayName("Should handle path with . and .. correctly within baseDir")
403+
void testValidatePath_DotPathWithinBaseDir() throws IOException {
404+
Path baseDir = tempDir;
405+
Path subDir = tempDir.resolve("subdir");
406+
Files.createDirectories(subDir);
407+
408+
// ./subdir/../test.txt should normalize to test.txt in baseDir
409+
Path result = FileToolUtils.validatePath("./subdir/../test.txt", baseDir);
410+
411+
assertNotNull(result);
412+
assertTrue(result.startsWith(baseDir), "Result should be within baseDir");
413+
assertEquals(baseDir.resolve("test.txt"), result, "Should normalize correctly");
414+
}
254415
}

agentscope-core/src/test/java/io/agentscope/core/tool/file/WriteFileToolTest.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,68 @@ void testWriteTextFile_CreateNewFileWithinBaseDir() throws IOException {
669669
assertEquals("New file content", content);
670670
}
671671

672+
@Test
673+
@DisplayName("Should handle relative path with baseDir correctly")
674+
void testWriteTextFile_RelativePathWithBaseDir() throws IOException {
675+
WriteFileTool tool = new WriteFileTool(tempDir.toString());
676+
677+
// Use relative path - should be resolved relative to baseDir
678+
Mono<ToolResultBlock> result =
679+
tool.writeTextFile("subdir/relative.txt", "Content from relative path", null);
680+
681+
StepVerifier.create(result)
682+
.assertNext(
683+
block -> {
684+
assertNotNull(block);
685+
String text = extractText(block);
686+
assertTrue(
687+
text.contains("Create and write")
688+
&& text.contains("successfully"),
689+
"Should successfully create file with relative path");
690+
})
691+
.verifyComplete();
692+
693+
// Verify file was created in the correct location (relative to baseDir)
694+
Path expectedFile = tempDir.resolve("subdir/relative.txt");
695+
assertTrue(
696+
Files.exists(expectedFile),
697+
"File should be created relative to baseDir: " + expectedFile);
698+
String content = Files.readString(expectedFile);
699+
assertEquals("Content from relative path", content);
700+
}
701+
702+
@Test
703+
@DisplayName("Should handle relative path when reading with baseDir")
704+
void testInsertTextFile_RelativePathWithBaseDir() throws IOException {
705+
// Create a test file using relative path
706+
WriteFileTool tool = new WriteFileTool(tempDir.toString());
707+
tool.writeTextFile("test_relative.txt", "Line 1\nLine 2\nLine 3", null).block();
708+
709+
// Insert content using relative path
710+
Mono<ToolResultBlock> result = tool.insertTextFile("test_relative.txt", "Inserted Line", 2);
711+
712+
StepVerifier.create(result)
713+
.assertNext(
714+
block -> {
715+
assertNotNull(block);
716+
String text = extractText(block);
717+
assertTrue(
718+
text.contains("Insert content")
719+
&& text.contains("successfully"),
720+
"Should successfully insert with relative path");
721+
})
722+
.verifyComplete();
723+
724+
// Verify content was inserted correctly
725+
Path expectedFile = tempDir.resolve("test_relative.txt");
726+
List<String> lines = Files.readAllLines(expectedFile);
727+
assertEquals(4, lines.size());
728+
assertEquals("Line 1", lines.get(0));
729+
assertEquals("Inserted Line", lines.get(1));
730+
assertEquals("Line 2", lines.get(2));
731+
assertEquals("Line 3", lines.get(3));
732+
}
733+
672734
/**
673735
* Extract text content from ToolResultBlock for assertion.
674736
*/

0 commit comments

Comments
 (0)