Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/arkruntime/selfhosted/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ class ReadFileTool(Tool):
def execute(self, tool_input: Any, context: ToolContext) -> ToolResult:
args = _as_mapping(tool_input)
try:
path = _safe_path(context, _read_file_path(args))
path = _safe_path(context, _file_path(args))
except ValueError as exc:
return error_result(str(exc))
view_range = args.get("view_range")
Expand Down Expand Up @@ -239,7 +239,7 @@ def execute(self, tool_input: Any, context: ToolContext) -> ToolResult:
return text_result(_render_read_lines(text, offset, limit))


def _read_file_path(args: Mapping[str, Any]) -> str:
def _file_path(args: Mapping[str, Any]) -> str:
path = ""
for name in ("file_path", "path", "file"):
value = args.get(name)
Expand Down Expand Up @@ -329,8 +329,11 @@ class WriteFileTool(Tool):

def execute(self, tool_input: Any, context: ToolContext) -> ToolResult:
args = _as_mapping(tool_input)
raw_path = str(args.get("path") or args.get("file") or "")
path = _safe_path(context, raw_path)
try:
raw_path = _file_path(args)
path = _safe_path(context, raw_path)
except ValueError as exc:
return error_result(str(exc))
content = str(args.get("content") or "")
Path(path).parent.mkdir(parents=True, exist_ok=True)
verified = _safe_path(context, raw_path)
Expand All @@ -345,8 +348,11 @@ class EditFileTool(Tool):

def execute(self, tool_input: Any, context: ToolContext) -> ToolResult:
args = _as_mapping(tool_input)
raw_path = str(args.get("path") or args.get("file") or "")
path = _safe_path(context, raw_path)
try:
raw_path = _file_path(args)
path = _safe_path(context, raw_path)
except ValueError as exc:
return error_result(str(exc))
old = str(args.get("old_string") or args.get("old") or "")
new = str(args.get("new_string") or args.get("new") or "")
if not old:
Expand Down
44 changes: 44 additions & 0 deletions tests/selfhosted/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,50 @@ def test_read_keeps_legacy_inputs_without_ambiguity(tmp_path) -> None:
assert _text(range_conflict) == "view_range cannot be combined with offset or limit"


@pytest.mark.parametrize("path_key", ["file_path", "path", "file"])
def test_file_tools_accept_path_aliases(tmp_path, path_key) -> None:
context = ToolContext(workdir=str(tmp_path))
source = tmp_path / "source.txt"
source.write_text("old")

read = ReadFileTool().execute({path_key: "source.txt"}, context)
write = WriteFileTool().execute({path_key: "written.txt", "content": "created"}, context)
edit = EditFileTool().execute(
{path_key: "source.txt", "old_string": "old", "new_string": "new"},
context,
)

assert not read.is_error
assert not write.is_error
assert not edit.is_error
assert (tmp_path / "written.txt").read_text() == "created"
assert source.read_text() == "new"


@pytest.mark.parametrize("tool", [ReadFileTool(), WriteFileTool(), EditFileTool()])
def test_file_tools_validate_path_aliases(tmp_path, tool) -> None:
context = ToolContext(workdir=str(tmp_path))
(tmp_path / "example.txt").write_text("old")
common = {
"file_path": "example.txt",
"path": "example.txt",
"file": "example.txt",
"content": "new",
"old_string": "old",
"new_string": "new",
}

same = tool.execute(common, context)
conflict = tool.execute({**common, "path": "other.txt"}, context)
missing = tool.execute({"content": "new", "old_string": "old", "new_string": "new"}, context)

assert not same.is_error
assert conflict.is_error
assert _text(conflict) == "file_path, path, and file must not conflict"
assert missing.is_error
assert _text(missing) == "file_path is required"


def test_grep_skips_symlink_that_escapes_workdir(tmp_path) -> None:
outside = tmp_path.parent / f"{tmp_path.name}-outside"
outside.mkdir()
Expand Down
Loading