mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 19:23:42 +08:00
fix: resolve workspace context for file mutations (#617)
This commit is contained in:
parent
8d32a60fdb
commit
14df331ce6
@ -3,14 +3,18 @@ package vip.mate.tool.builtin;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.i18n.I18nService;
|
||||
import vip.mate.tool.ConcurrencyUnsafe;
|
||||
import vip.mate.tool.guard.WorkspacePathGuard;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 内置工具:编辑文件(查找替换)
|
||||
@ -31,9 +35,9 @@ import java.nio.file.Paths;
|
||||
@lombok.RequiredArgsConstructor
|
||||
public class EditFileTool {
|
||||
|
||||
private final vip.mate.i18n.I18nService i18n;
|
||||
private final I18nService i18n;
|
||||
|
||||
@vip.mate.tool.ConcurrencyUnsafe("in-place file edit — must not race with reads/writes on the same path")
|
||||
@ConcurrencyUnsafe("in-place file edit — must not race with reads/writes on the same path")
|
||||
@Tool(description = "Edit file content via find-and-replace. Finds exact match of old_text and replaces with new_text. "
|
||||
+ "Returns structured JSON with filePath, replacements count. "
|
||||
+ "May require user approval when security rules flag the edit. "
|
||||
@ -42,7 +46,8 @@ public class EditFileTool {
|
||||
@ToolParam(description = "Absolute or relative file path") String filePath,
|
||||
@ToolParam(description = "Original text to find (exact match)") String oldText,
|
||||
@ToolParam(description = "Replacement text") String newText,
|
||||
@ToolParam(description = "Replace all occurrences, default false (first only)", required = false) Boolean replaceAll) {
|
||||
@ToolParam(description = "Replace all occurrences, default false (first only)", required = false) Boolean replaceAll,
|
||||
@Nullable ToolContext ctx) {
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
result.set("filePath", filePath);
|
||||
@ -63,7 +68,7 @@ public class EditFileTool {
|
||||
|
||||
Path path;
|
||||
try {
|
||||
path = vip.mate.tool.guard.WorkspacePathGuard.validatePath(filePath);
|
||||
path = WorkspacePathGuard.validatePath(filePath, ctx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return errorResult(filePath, e.getMessage());
|
||||
}
|
||||
|
||||
@ -3,14 +3,18 @@ package vip.mate.tool.builtin;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.model.ToolContext;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.ai.tool.annotation.ToolParam;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import vip.mate.i18n.I18nService;
|
||||
import vip.mate.tool.ConcurrencyUnsafe;
|
||||
import vip.mate.tool.guard.WorkspacePathGuard;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* 内置工具:写入文件
|
||||
@ -31,15 +35,16 @@ import java.nio.file.Paths;
|
||||
@lombok.RequiredArgsConstructor
|
||||
public class WriteFileTool {
|
||||
|
||||
private final vip.mate.i18n.I18nService i18n;
|
||||
private final I18nService i18n;
|
||||
|
||||
@vip.mate.tool.ConcurrencyUnsafe("file write — must serialize with reads/writes on overlapping paths")
|
||||
@ConcurrencyUnsafe("file write — must serialize with reads/writes on overlapping paths")
|
||||
@Tool(description = "Write content to a file. Overwrites if exists, creates if not (auto-creates parent directories). "
|
||||
+ "Returns structured JSON with filePath, bytesWritten. "
|
||||
+ "May require user approval when security rules flag the write.")
|
||||
public String write_file(
|
||||
@ToolParam(description = "Absolute or relative file path") String filePath,
|
||||
@ToolParam(description = "Content to write to the file") String content) {
|
||||
@ToolParam(description = "Content to write to the file") String content,
|
||||
@Nullable ToolContext ctx) {
|
||||
|
||||
JSONObject result = new JSONObject();
|
||||
result.set("filePath", filePath);
|
||||
@ -54,7 +59,7 @@ public class WriteFileTool {
|
||||
|
||||
Path path;
|
||||
try {
|
||||
path = vip.mate.tool.guard.WorkspacePathGuard.validatePath(filePath);
|
||||
path = WorkspacePathGuard.validatePath(filePath, ctx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return errorResult(filePath, e.getMessage());
|
||||
}
|
||||
|
||||
@ -0,0 +1,75 @@
|
||||
package vip.mate.tool.builtin;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import vip.mate.agent.context.ChatOrigin;
|
||||
import vip.mate.i18n.I18nService;
|
||||
import vip.mate.tool.guard.WorkspacePathGuard;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Regression coverage for issue #617: file mutation tools must use the
|
||||
* explicit Spring AI ToolContext instead of relying on legacy thread-local
|
||||
* workspace state.
|
||||
*/
|
||||
class FileMutationToolContextTest {
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
WorkspacePathGuard.setDefaultRoot(null);
|
||||
ToolExecutionContext.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("write_file resolves relative paths against ToolContext workspace root (#617)")
|
||||
void writeFileUsesToolContextWorkspaceRoot(@TempDir Path tempDir) throws Exception {
|
||||
Path defaultRoot = Files.createDirectory(tempDir.resolve("default-root"));
|
||||
Path contextRoot = Files.createDirectory(tempDir.resolve("context-root"));
|
||||
WorkspacePathGuard.setDefaultRoot(defaultRoot.toString());
|
||||
WriteFileTool tool = new WriteFileTool(i18n());
|
||||
|
||||
String result = tool.write_file(
|
||||
"deck.md",
|
||||
"# Deck",
|
||||
ChatOrigin.web("conv-617", "alice", 1L, contextRoot.toString()).toToolContext());
|
||||
|
||||
assertThat(JSONUtil.parseObj(result).getBool("error", false)).isFalse();
|
||||
assertThat(contextRoot.resolve("deck.md")).hasContent("# Deck");
|
||||
assertThat(defaultRoot.resolve("deck.md")).doesNotExist();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("edit_file resolves relative paths against ToolContext workspace root (#617)")
|
||||
void editFileUsesToolContextWorkspaceRoot(@TempDir Path tempDir) throws Exception {
|
||||
Path defaultRoot = Files.createDirectory(tempDir.resolve("default-root"));
|
||||
Path contextRoot = Files.createDirectory(tempDir.resolve("context-root"));
|
||||
WorkspacePathGuard.setDefaultRoot(defaultRoot.toString());
|
||||
Files.writeString(contextRoot.resolve("deck.md"), "old title", StandardCharsets.UTF_8);
|
||||
EditFileTool tool = new EditFileTool(i18n());
|
||||
|
||||
String result = tool.edit_file(
|
||||
"deck.md",
|
||||
"old",
|
||||
"new",
|
||||
false,
|
||||
ChatOrigin.web("conv-617", "alice", 1L, contextRoot.toString()).toToolContext());
|
||||
|
||||
assertThat(JSONUtil.parseObj(result).getBool("error", false)).isFalse();
|
||||
assertThat(contextRoot.resolve("deck.md")).hasContent("new title");
|
||||
assertThat(defaultRoot.resolve("deck.md")).doesNotExist();
|
||||
}
|
||||
|
||||
private static I18nService i18n() {
|
||||
return mock(I18nService.class, inv ->
|
||||
"msg".equals(inv.getMethod().getName()) ? inv.getArgument(0) : null);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user