tenseleyflow/loader / 51bdf4d

Browse files

Remove workspace boundary from read-only tools (read, glob, grep)

Authored by espadonne
SHA
51bdf4d46dcced9543f2b13a42af74da82dbedcf
Parents
9a32994
Tree
4bcac94

4 changed files

StatusFile+-
M src/loader/tools/file_tools.py 4 6
M src/loader/tools/search_tools.py 2 3
M tests/test_tool_safety.py 6 6
M tests/test_turn_iteration.py 2 2
src/loader/tools/file_tools.pymodified
@@ -68,14 +68,13 @@ class ReadTool(Tool):
6868
         **kwargs: Any,
6969
     ) -> ToolResult:
7070
         try:
71
+            # Reads are safe — don't enforce workspace boundary
7172
             path = resolve_workspace_path(
7273
                 file_path,
73
-                workspace_root=self.workspace_root,
74
+                workspace_root=None,
7475
             )
7576
         except FileNotFoundError:
7677
             return ToolResult(f"File not found: {file_path}", is_error=True)
77
-        except PermissionError as exc:
78
-            return ToolResult(f"Permission denied: {exc}", is_error=True)
7978
         except Exception as exc:
8079
             return ToolResult(f"Error resolving file path: {exc}", is_error=True)
8180
 
@@ -552,14 +551,13 @@ class GlobTool(Tool):
552551
         **kwargs: Any,
553552
     ) -> ToolResult:
554553
         try:
554
+            # Glob is read-only — don't enforce workspace boundary
555555
             base_path = resolve_workspace_path(
556556
                 path,
557
-                workspace_root=self.workspace_root,
557
+                workspace_root=None,
558558
             )
559559
         except FileNotFoundError:
560560
             return ToolResult(f"Directory not found: {path}", is_error=True)
561
-        except PermissionError as exc:
562
-            return ToolResult(f"Permission denied: {exc}", is_error=True)
563561
         except Exception as exc:
564562
             return ToolResult(f"Error resolving directory: {exc}", is_error=True)
565563
 
src/loader/tools/search_tools.pymodified
@@ -73,14 +73,13 @@ class GrepTool(Tool):
7373
         **kwargs: Any,
7474
     ) -> ToolResult:
7575
         try:
76
+            # Grep is read-only — don't enforce workspace boundary
7677
             base_path = resolve_workspace_path(
7778
                 path,
78
-                workspace_root=self.workspace_root,
79
+                workspace_root=None,
7980
             )
8081
         except FileNotFoundError:
8182
             return ToolResult(f"Path not found: {path}", is_error=True)
82
-        except PermissionError as exc:
83
-            return ToolResult(f"Permission denied: {exc}", is_error=True)
8483
         except Exception as exc:
8584
             return ToolResult(f"Error resolving search path: {exc}", is_error=True)
8685
 
tests/test_tool_safety.pymodified
@@ -25,17 +25,17 @@ async def test_read_tool_blocks_binary_file(temp_dir: Path) -> None:
2525
 
2626
 
2727
 @pytest.mark.asyncio
28
-async def test_read_tool_blocks_symlink_escape(temp_dir: Path) -> None:
28
+async def test_read_tool_allows_outside_workspace(temp_dir: Path) -> None:
29
+    """Reads are safe and should not enforce workspace boundaries."""
2930
     outside = temp_dir.parent / "outside.txt"
3031
     outside.write_text("outside\n")
31
-    inside_link = temp_dir / "escape.txt"
32
-    inside_link.symlink_to(outside)
3332
     tool = ReadTool(workspace_root=temp_dir)
3433
 
35
-    result = await tool.execute(file_path=str(inside_link))
34
+    result = await tool.execute(file_path=str(outside))
3635
 
37
-    assert result.is_error
38
-    assert "workspace boundary" in result.output.lower()
36
+    assert not result.is_error
37
+    assert "outside" in result.output
38
+    outside.unlink()
3939
 
4040
 
4141
 @pytest.mark.asyncio
tests/test_turn_iteration.pymodified
@@ -110,7 +110,7 @@ async def test_turn_iteration_executes_native_tool_batch_and_continues(
110110
                     ToolCall(
111111
                         id="read-1",
112112
                         name="read",
113
-                        arguments={"file_path": "README.md"},
113
+                        arguments={"file_path": str(readme)},
114114
                     )
115115
                 ],
116116
             )
@@ -129,7 +129,7 @@ async def test_turn_iteration_executes_native_tool_batch_and_continues(
129129
     )
130130
 
131131
     assert decision.action == TurnIterationAction.CONTINUE
132
-    assert decision.new_actions_taken == ["read: {'file_path': 'README.md'}"]
132
+    assert decision.new_actions_taken == [f"read: {{'file_path': '{readme}'}}"]
133133
     assert prepared.summary.assistant_messages[-1].tool_calls[0].name == "read"
134134
     assert len(prepared.summary.tool_result_messages) == 1
135135
     assert "Loader runtime notes" in prepared.summary.tool_result_messages[0].content