vibecore 0.3.0__py3-none-any.whl → 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. vibecore/agents/default.py +3 -3
  2. vibecore/agents/task.py +3 -3
  3. vibecore/cli.py +67 -43
  4. vibecore/context.py +74 -11
  5. vibecore/flow.py +335 -73
  6. vibecore/handlers/stream_handler.py +35 -56
  7. vibecore/main.py +70 -272
  8. vibecore/session/jsonl_session.py +3 -1
  9. vibecore/session/loader.py +2 -2
  10. vibecore/settings.py +48 -1
  11. vibecore/tools/file/executor.py +59 -13
  12. vibecore/tools/file/tools.py +9 -9
  13. vibecore/tools/path_validator.py +251 -0
  14. vibecore/tools/python/helpers.py +2 -2
  15. vibecore/tools/python/tools.py +2 -2
  16. vibecore/tools/shell/executor.py +63 -7
  17. vibecore/tools/shell/tools.py +9 -9
  18. vibecore/tools/task/executor.py +2 -2
  19. vibecore/tools/task/tools.py +2 -2
  20. vibecore/tools/todo/manager.py +2 -10
  21. vibecore/tools/todo/models.py +5 -14
  22. vibecore/tools/todo/tools.py +5 -5
  23. vibecore/tools/webfetch/tools.py +1 -4
  24. vibecore/tools/websearch/ddgs/backend.py +1 -1
  25. vibecore/tools/websearch/tools.py +1 -4
  26. vibecore/widgets/core.py +3 -17
  27. vibecore/widgets/feedback.py +164 -0
  28. vibecore/widgets/feedback.tcss +121 -0
  29. vibecore/widgets/messages.py +22 -2
  30. vibecore/widgets/messages.tcss +28 -0
  31. vibecore/widgets/tool_messages.py +19 -4
  32. vibecore/widgets/tool_messages.tcss +23 -0
  33. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/METADATA +122 -29
  34. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/RECORD +37 -34
  35. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/WHEEL +0 -0
  36. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/entry_points.txt +0 -0
  37. {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/licenses/LICENSE +0 -0
@@ -2,13 +2,22 @@
2
2
 
3
3
  from typing import Any
4
4
 
5
- from .utils import PathValidationError, format_line_with_number, validate_file_path
5
+ from agents import RunContextWrapper
6
6
 
7
+ from vibecore.context import PathValidatorContext
8
+ from vibecore.settings import settings
9
+ from vibecore.tools.file.utils import PathValidationError
7
10
 
8
- async def read_file(file_path: str, offset: int | None = None, limit: int | None = None) -> str:
11
+ from .utils import format_line_with_number
12
+
13
+
14
+ async def read_file(
15
+ ctx: RunContextWrapper[PathValidatorContext], file_path: str, offset: int | None = None, limit: int | None = None
16
+ ) -> str:
9
17
  """Read a file and return its contents in cat -n format.
10
18
 
11
19
  Args:
20
+ ctx: The context wrapper containing the VibecoreContext
12
21
  file_path: The path to the file to read
13
22
  offset: The line number to start reading from (1-based)
14
23
  limit: The maximum number of lines to read
@@ -17,8 +26,14 @@ async def read_file(file_path: str, offset: int | None = None, limit: int | None
17
26
  The file contents with line numbers, or an error message
18
27
  """
19
28
  try:
20
- # Validate the file path
21
- validated_path = validate_file_path(file_path)
29
+ # Validate the file path using context if path confinement is enabled
30
+ if settings.path_confinement.enabled:
31
+ validated_path = ctx.context.path_validator.validate_path(file_path, operation="read")
32
+ else:
33
+ # Fall back to simple validation against CWD
34
+ from .utils import validate_file_path
35
+
36
+ validated_path = validate_file_path(file_path)
22
37
 
23
38
  # Check if file exists
24
39
  if not validated_path.exists():
@@ -84,10 +99,17 @@ async def read_file(file_path: str, offset: int | None = None, limit: int | None
84
99
  return f"Error: Unexpected error reading file: {e}"
85
100
 
86
101
 
87
- async def edit_file(file_path: str, old_string: str, new_string: str, replace_all: bool = False) -> str:
102
+ async def edit_file(
103
+ ctx: RunContextWrapper[PathValidatorContext],
104
+ file_path: str,
105
+ old_string: str,
106
+ new_string: str,
107
+ replace_all: bool = False,
108
+ ) -> str:
88
109
  """Edit a file by replacing strings.
89
110
 
90
111
  Args:
112
+ ctx: The context wrapper containing the VibecoreContext
91
113
  file_path: The path to the file to edit
92
114
  old_string: The text to replace
93
115
  new_string: The text to replace it with
@@ -97,8 +119,14 @@ async def edit_file(file_path: str, old_string: str, new_string: str, replace_al
97
119
  Success message or error message
98
120
  """
99
121
  try:
100
- # Validate the file path
101
- validated_path = validate_file_path(file_path)
122
+ # Validate the file path using context if path confinement is enabled
123
+ if settings.path_confinement.enabled:
124
+ validated_path = ctx.context.path_validator.validate_path(file_path, operation="edit")
125
+ else:
126
+ # Fall back to simple validation against CWD
127
+ from .utils import validate_file_path
128
+
129
+ validated_path = validate_file_path(file_path)
102
130
 
103
131
  # Check if file exists
104
132
  if not validated_path.exists():
@@ -158,10 +186,15 @@ async def edit_file(file_path: str, old_string: str, new_string: str, replace_al
158
186
  return f"Error: Unexpected error editing file: {e}"
159
187
 
160
188
 
161
- async def multi_edit_file(file_path: str, edits: list[dict[str, Any]]) -> str:
189
+ async def multi_edit_file(
190
+ ctx: RunContextWrapper[PathValidatorContext],
191
+ file_path: str,
192
+ edits: list[dict[str, Any]],
193
+ ) -> str:
162
194
  """Edit a file by applying multiple replacements sequentially.
163
195
 
164
196
  Args:
197
+ ctx: The context wrapper containing the VibecoreContext
165
198
  file_path: The path to the file to edit
166
199
  edits: List of edit operations, each containing old_string, new_string, and optional replace_all
167
200
 
@@ -169,8 +202,14 @@ async def multi_edit_file(file_path: str, edits: list[dict[str, Any]]) -> str:
169
202
  Success message or error message
170
203
  """
171
204
  try:
172
- # Validate the file path
173
- validated_path = validate_file_path(file_path)
205
+ # Validate the file path using context if path confinement is enabled
206
+ if settings.path_confinement.enabled:
207
+ validated_path = ctx.context.path_validator.validate_path(file_path, operation="multi_edit")
208
+ else:
209
+ # Fall back to simple validation against CWD
210
+ from .utils import validate_file_path
211
+
212
+ validated_path = validate_file_path(file_path)
174
213
 
175
214
  # Check if file exists
176
215
  if not validated_path.exists():
@@ -239,10 +278,11 @@ async def multi_edit_file(file_path: str, edits: list[dict[str, Any]]) -> str:
239
278
  return f"Error: Unexpected error editing file: {e}"
240
279
 
241
280
 
242
- async def write_file(file_path: str, content: str) -> str:
281
+ async def write_file(ctx: RunContextWrapper[PathValidatorContext], file_path: str, content: str) -> str:
243
282
  """Write content to a file.
244
283
 
245
284
  Args:
285
+ ctx: The context wrapper containing the VibecoreContext
246
286
  file_path: The path to the file to write
247
287
  content: The content to write to the file
248
288
 
@@ -250,8 +290,14 @@ async def write_file(file_path: str, content: str) -> str:
250
290
  Success message or error message
251
291
  """
252
292
  try:
253
- # Validate the file path
254
- validated_path = validate_file_path(file_path)
293
+ # Validate the file path using context if path confinement is enabled
294
+ if settings.path_confinement.enabled:
295
+ validated_path = ctx.context.path_validator.validate_path(file_path, operation="write")
296
+ else:
297
+ # Fall back to simple validation against CWD
298
+ from .utils import validate_file_path
299
+
300
+ validated_path = validate_file_path(file_path)
255
301
 
256
302
  # Check if it's a directory
257
303
  if validated_path.exists() and validated_path.is_dir():
@@ -3,7 +3,7 @@
3
3
  from agents import RunContextWrapper, function_tool
4
4
  from pydantic import BaseModel
5
5
 
6
- from vibecore.context import VibecoreContext
6
+ from vibecore.context import PathValidatorContext
7
7
 
8
8
  from .executor import edit_file, multi_edit_file, read_file, write_file
9
9
 
@@ -18,7 +18,7 @@ class EditOperation(BaseModel):
18
18
 
19
19
  @function_tool
20
20
  async def read(
21
- ctx: RunContextWrapper[VibecoreContext],
21
+ ctx: RunContextWrapper[PathValidatorContext],
22
22
  file_path: str,
23
23
  offset: int | None = None,
24
24
  limit: int | None = None,
@@ -49,12 +49,12 @@ async def read(
49
49
  Returns:
50
50
  The file contents with line numbers in cat -n format, or an error message
51
51
  """
52
- return await read_file(file_path, offset, limit)
52
+ return await read_file(ctx, file_path, offset, limit)
53
53
 
54
54
 
55
55
  @function_tool
56
56
  async def edit(
57
- ctx: RunContextWrapper[VibecoreContext],
57
+ ctx: RunContextWrapper[PathValidatorContext],
58
58
  file_path: str,
59
59
  old_string: str,
60
60
  new_string: str,
@@ -86,12 +86,12 @@ async def edit(
86
86
  Returns:
87
87
  Success message or error message
88
88
  """
89
- return await edit_file(file_path, old_string, new_string, replace_all)
89
+ return await edit_file(ctx, file_path, old_string, new_string, replace_all)
90
90
 
91
91
 
92
92
  @function_tool
93
93
  async def multi_edit(
94
- ctx: RunContextWrapper[VibecoreContext],
94
+ ctx: RunContextWrapper[PathValidatorContext],
95
95
  file_path: str,
96
96
  edits: list[EditOperation],
97
97
  ) -> str:
@@ -153,12 +153,12 @@ async def multi_edit(
153
153
  """
154
154
  # Convert EditOperation objects to dictionaries
155
155
  edit_dicts = [edit.model_dump() for edit in edits]
156
- return await multi_edit_file(file_path, edit_dicts)
156
+ return await multi_edit_file(ctx, file_path, edit_dicts)
157
157
 
158
158
 
159
159
  @function_tool
160
160
  async def write(
161
- ctx: RunContextWrapper[VibecoreContext],
161
+ ctx: RunContextWrapper[PathValidatorContext],
162
162
  file_path: str,
163
163
  content: str,
164
164
  ) -> str:
@@ -181,4 +181,4 @@ async def write(
181
181
  Returns:
182
182
  Success message or error message
183
183
  """
184
- return await write_file(file_path, content)
184
+ return await write_file(ctx, file_path, content)
@@ -0,0 +1,251 @@
1
+ """Path validation module for vibecore tools.
2
+
3
+ This module provides path validation functionality to confine file and shell
4
+ operations to a configurable list of allowed directories.
5
+ """
6
+
7
+ import shlex
8
+ from contextlib import suppress
9
+ from pathlib import Path
10
+
11
+ from textual import log
12
+
13
+ from vibecore.tools.file.utils import PathValidationError
14
+
15
+
16
+ class PathValidator:
17
+ """Validates paths against a list of allowed directories."""
18
+
19
+ def __init__(self, allowed_directories: list[Path]):
20
+ """Initialize with list of allowed directories.
21
+
22
+ Args:
23
+ allowed_directories: List of directories to allow access to.
24
+ Defaults to [CWD] if empty.
25
+ """
26
+ self.allowed_directories = (
27
+ [d.resolve() for d in allowed_directories] if allowed_directories else [Path.cwd().resolve()]
28
+ )
29
+
30
+ def validate_path(self, path: str | Path, operation: str = "access") -> Path:
31
+ """Validate a path against allowed directories.
32
+
33
+ Args:
34
+ path: The path to validate
35
+ operation: Description of the operation (for error messages)
36
+
37
+ Returns:
38
+ The validated absolute Path object
39
+
40
+ Raises:
41
+ PathValidationError: If path is outside allowed directories
42
+ """
43
+ # Convert to Path object
44
+ path_obj = Path(path) if isinstance(path, str) else path
45
+
46
+ # Resolve to absolute path (follows symlinks)
47
+ try:
48
+ absolute_path = path_obj.resolve()
49
+ except (OSError, RuntimeError) as e:
50
+ # Handle cases where path resolution fails
51
+ raise PathValidationError(f"Cannot resolve path '{path}': {e}") from e
52
+
53
+ # Check if path is under any allowed directory
54
+ if not self.is_path_allowed(absolute_path):
55
+ allowed_dirs_str = ", ".join(f"'{d}'" for d in self.allowed_directories)
56
+ raise PathValidationError(
57
+ f"Path '{absolute_path}' is outside the allowed directories. "
58
+ f"Access is restricted to {allowed_dirs_str} and their subdirectories."
59
+ )
60
+
61
+ return absolute_path
62
+
63
+ def validate_command_paths(self, command: str) -> None:
64
+ """Validate paths referenced in a shell command.
65
+
66
+ Args:
67
+ command: The shell command to validate
68
+
69
+ Raises:
70
+ PathValidationError: If command references paths outside allowed directories
71
+ """
72
+ # Parse the command to extract potential file paths
73
+ try:
74
+ # First, replace shell operators with spaces around them to ensure proper splitting
75
+ # This handles cases like "cd /path;ls" which shlex doesn't split properly
76
+ for op in [";", "&&", "||", "|", "&"]:
77
+ command = command.replace(op, f" {op} ")
78
+
79
+ # Use shlex to properly parse the command
80
+ tokens = shlex.split(command)
81
+ except ValueError as e:
82
+ # If shlex fails, the command might be malformed
83
+ raise PathValidationError(f"Cannot parse command: {e}") from e
84
+
85
+ # Commands that take path arguments
86
+ path_commands = {
87
+ "cat",
88
+ "ls",
89
+ "cd",
90
+ "cp",
91
+ "mv",
92
+ "rm",
93
+ "mkdir",
94
+ "rmdir",
95
+ "touch",
96
+ "chmod",
97
+ "chown",
98
+ "head",
99
+ "tail",
100
+ "less",
101
+ "more",
102
+ "grep",
103
+ "find",
104
+ "sed",
105
+ "awk",
106
+ "wc",
107
+ "du",
108
+ "df",
109
+ "tar",
110
+ "zip",
111
+ "unzip",
112
+ "vim",
113
+ "vi",
114
+ "nano",
115
+ "emacs",
116
+ "code",
117
+ "open",
118
+ }
119
+
120
+ # Check each token that might be a path
121
+ current_command = None
122
+ piped_command = False # Track if command comes after a pipe
123
+ for i, token in enumerate(tokens):
124
+ # Skip shell operators
125
+ if token in ["&&", "||", ";", "|", "&", ">", ">>", "<", "2>", "&>"]:
126
+ if token == "|":
127
+ piped_command = True
128
+ elif token in ["&&", "||", ";"]:
129
+ piped_command = False
130
+ continue
131
+
132
+ # Skip flags and options
133
+ if token.startswith("-"):
134
+ continue
135
+
136
+ # Check if this is a command
137
+ if i == 0 or tokens[i - 1] in ["&&", "||", ";", "|"]:
138
+ current_command = token.split("/")[-1] # Get base command name
139
+ # Don't validate grep/awk/sed arguments after pipes - they're patterns not paths
140
+ if piped_command and current_command in ["grep", "awk", "sed", "sort", "uniq", "wc"]:
141
+ current_command = None
142
+ if tokens[i - 1] in ["&&", "||", ";"]:
143
+ piped_command = False
144
+ continue
145
+
146
+ # Check for redirections
147
+ if i > 0 and tokens[i - 1] in [">", ">>", "<", "2>", "&>"]:
148
+ # This is a file path for redirection
149
+ self._validate_path_token(token, f"redirect to/from '{token}'")
150
+ continue
151
+
152
+ # Check if current command takes path arguments
153
+ if current_command in path_commands:
154
+ # Skip if it looks like an option value
155
+ if i > 0 and tokens[i - 1].startswith("-"):
156
+ continue
157
+ # This might be a path argument
158
+ self._validate_path_token(token, f"access '{token}'")
159
+
160
+ # Check for paths in other contexts (if they look like paths)
161
+ elif "/" in token or token in [".", "..", "~"]:
162
+ # This looks like a path, validate it
163
+ with suppress(PathValidationError):
164
+ # It might not be a path, just a string with slash
165
+ # We'll be lenient here if it fails
166
+ self._validate_path_token(token, f"access '{token}'")
167
+
168
+ def _validate_path_token(self, token: str, operation: str) -> None:
169
+ """Validate a single path token from a command.
170
+
171
+ Args:
172
+ token: The token that might be a path
173
+ operation: Description of the operation
174
+
175
+ Raises:
176
+ PathValidationError: If the path is not allowed
177
+ """
178
+ # Expand user home directory
179
+ if token.startswith("~"):
180
+ token = str(Path(token).expanduser())
181
+
182
+ # Skip URLs and remote paths
183
+ if (
184
+ token.startswith("http://")
185
+ or token.startswith("https://")
186
+ or token.startswith("ftp://")
187
+ or token.startswith("ssh://")
188
+ or token.startswith("git@")
189
+ or ":" in token.split("/")[0]
190
+ ): # user@host:path
191
+ return
192
+
193
+ # Try to validate as a path
194
+ try:
195
+ path = Path(token)
196
+ # If it's a relative path, resolve it from CWD
197
+ if not path.is_absolute():
198
+ path = Path.cwd() / path
199
+ self.validate_path(path, operation)
200
+ except (ValueError, OSError):
201
+ # Not a valid path, skip validation
202
+ pass
203
+
204
+ def is_path_allowed(self, path: Path) -> bool:
205
+ """Check if a path is within allowed directories.
206
+
207
+ Args:
208
+ path: The path to check (should be absolute)
209
+
210
+ Returns:
211
+ True if path is allowed, False otherwise
212
+ """
213
+ # Ensure path is absolute
214
+ path = path.resolve()
215
+ log(f"Validating path: {path}")
216
+
217
+ # Check if path is under any allowed directory
218
+ for allowed_dir in self.allowed_directories:
219
+ try:
220
+ # Check if path is relative to allowed_dir
221
+ path.relative_to(allowed_dir)
222
+ return True
223
+ except ValueError:
224
+ # path is not relative to this allowed_dir
225
+ continue
226
+
227
+ return False
228
+
229
+ def _is_parent_of(self, parent: Path, child: Path) -> bool:
230
+ """Check if parent is a parent directory of child.
231
+
232
+ Args:
233
+ parent: Potential parent path
234
+ child: Potential child path
235
+
236
+ Returns:
237
+ True if parent is a parent of child
238
+ """
239
+ try:
240
+ child.relative_to(parent)
241
+ return True
242
+ except ValueError:
243
+ return False
244
+
245
+ def get_allowed_directories(self) -> list[Path]:
246
+ """Get the list of allowed directories.
247
+
248
+ Returns:
249
+ List of allowed directory paths
250
+ """
251
+ return self.allowed_directories.copy()
@@ -5,7 +5,7 @@ from io import BytesIO
5
5
 
6
6
  from agents import RunContextWrapper
7
7
 
8
- from vibecore.context import VibecoreContext
8
+ from vibecore.context import PythonToolContext
9
9
 
10
10
  try:
11
11
  from PIL import Image # type: ignore[import-not-found]
@@ -16,7 +16,7 @@ except ImportError:
16
16
  TERM_IMAGE_AVAILABLE = False
17
17
 
18
18
 
19
- async def execute_python_helper(ctx: RunContextWrapper[VibecoreContext], code: str) -> str:
19
+ async def execute_python_helper(ctx: RunContextWrapper[PythonToolContext], code: str) -> str:
20
20
  """Helper function to execute Python code.
21
21
 
22
22
  This is the actual implementation extracted from the tool decorator.
@@ -2,13 +2,13 @@
2
2
 
3
3
  from agents import RunContextWrapper, function_tool
4
4
 
5
- from vibecore.context import VibecoreContext
5
+ from vibecore.context import PythonToolContext
6
6
 
7
7
  from .helpers import execute_python_helper
8
8
 
9
9
 
10
10
  @function_tool
11
- async def execute_python(ctx: RunContextWrapper[VibecoreContext], code: str) -> str:
11
+ async def execute_python(ctx: RunContextWrapper[PythonToolContext], code: str) -> str:
12
12
  """Execute Python code with persistent context across the session.
13
13
 
14
14
  The execution environment maintains state between calls, allowing you to:
@@ -6,13 +6,20 @@ import re
6
6
  import subprocess
7
7
  from pathlib import Path
8
8
 
9
+ from agents import RunContextWrapper
10
+
11
+ from vibecore.context import PathValidatorContext
12
+ from vibecore.settings import settings
9
13
  from vibecore.tools.file.utils import PathValidationError, validate_file_path
10
14
 
11
15
 
12
- async def bash_executor(command: str, timeout: int | None = None) -> tuple[str, int]:
16
+ async def bash_executor(
17
+ ctx: RunContextWrapper[PathValidatorContext], command: str, timeout: int | None = None
18
+ ) -> tuple[str, int]:
13
19
  """Execute a bash command asynchronously.
14
20
 
15
21
  Args:
22
+ ctx: The context wrapper containing the VibecoreContext
16
23
  command: The bash command to execute
17
24
  timeout: Optional timeout in milliseconds (max 600000)
18
25
 
@@ -32,6 +39,13 @@ async def bash_executor(command: str, timeout: int | None = None) -> tuple[str,
32
39
  # Convert timeout to seconds
33
40
  timeout_seconds = timeout / 1000.0
34
41
 
42
+ # Validate command paths if path confinement is enabled
43
+ if settings.path_confinement.enabled:
44
+ try:
45
+ ctx.context.path_validator.validate_command_paths(command)
46
+ except PathValidationError as e:
47
+ return f"Error: {e}", 1
48
+
35
49
  process = None
36
50
  try:
37
51
  # Create subprocess
@@ -66,10 +80,11 @@ async def bash_executor(command: str, timeout: int | None = None) -> tuple[str,
66
80
  return f"Error executing command: {e}", 1
67
81
 
68
82
 
69
- async def glob_files(pattern: str, path: str | None = None) -> list[str]:
83
+ async def glob_files(ctx: RunContextWrapper[PathValidatorContext], pattern: str, path: str | None = None) -> list[str]:
70
84
  """Find files matching a glob pattern.
71
85
 
72
86
  Args:
87
+ ctx: The context wrapper containing the VibecoreContext
73
88
  pattern: The glob pattern to match
74
89
  path: Optional directory to search in (defaults to CWD)
75
90
 
@@ -78,7 +93,23 @@ async def glob_files(pattern: str, path: str | None = None) -> list[str]:
78
93
  """
79
94
  try:
80
95
  # Validate and resolve the path
81
- search_path = Path.cwd() if path is None else validate_file_path(path)
96
+ if path is None:
97
+ search_path = Path.cwd()
98
+ # Validate CWD is in allowed directories if path confinement is enabled
99
+ if settings.path_confinement.enabled:
100
+ try:
101
+ ctx.context.path_validator.validate_path(search_path, operation="glob")
102
+ except PathValidationError as e:
103
+ return [f"Error: {e}"]
104
+ else:
105
+ # Validate the provided path
106
+ if settings.path_confinement.enabled:
107
+ try:
108
+ search_path = ctx.context.path_validator.validate_path(path, operation="glob")
109
+ except PathValidationError as e:
110
+ return [f"Error: {e}"]
111
+ else:
112
+ search_path = validate_file_path(path)
82
113
 
83
114
  # Validate path is a directory
84
115
  if not search_path.is_dir():
@@ -110,10 +141,13 @@ async def glob_files(pattern: str, path: str | None = None) -> list[str]:
110
141
  return [f"Error: {e}"]
111
142
 
112
143
 
113
- async def grep_files(pattern: str, path: str | None = None, include: str | None = None) -> list[str]:
144
+ async def grep_files(
145
+ ctx: RunContextWrapper[PathValidatorContext], pattern: str, path: str | None = None, include: str | None = None
146
+ ) -> list[str]:
114
147
  """Search file contents using regular expressions.
115
148
 
116
149
  Args:
150
+ ctx: The context wrapper containing the VibecoreContext
117
151
  pattern: The regex pattern to search for
118
152
  path: Directory to search in (defaults to CWD)
119
153
  include: File pattern to include (e.g. "*.js")
@@ -123,7 +157,23 @@ async def grep_files(pattern: str, path: str | None = None, include: str | None
123
157
  """
124
158
  try:
125
159
  # Validate and resolve the path
126
- search_path = Path.cwd() if path is None else validate_file_path(path)
160
+ if path is None:
161
+ search_path = Path.cwd()
162
+ # Validate CWD is in allowed directories if path confinement is enabled
163
+ if settings.path_confinement.enabled:
164
+ try:
165
+ ctx.context.path_validator.validate_path(search_path, operation="grep")
166
+ except PathValidationError as e:
167
+ return [f"Error: {e}"]
168
+ else:
169
+ # Validate the provided path
170
+ if settings.path_confinement.enabled:
171
+ try:
172
+ search_path = ctx.context.path_validator.validate_path(path, operation="grep")
173
+ except PathValidationError as e:
174
+ return [f"Error: {e}"]
175
+ else:
176
+ search_path = validate_file_path(path)
127
177
 
128
178
  # Validate path is a directory
129
179
  if not search_path.is_dir():
@@ -175,10 +225,13 @@ async def grep_files(pattern: str, path: str | None = None, include: str | None
175
225
  return [f"Error: {e}"]
176
226
 
177
227
 
178
- async def list_directory(path: str, ignore: list[str] | None = None) -> list[str]:
228
+ async def list_directory(
229
+ ctx: RunContextWrapper[PathValidatorContext], path: str, ignore: list[str] | None = None
230
+ ) -> list[str]:
179
231
  """List files and directories in a given path.
180
232
 
181
233
  Args:
234
+ ctx: The context wrapper containing the VibecoreContext
182
235
  path: The absolute path to list
183
236
  ignore: Optional list of glob patterns to ignore
184
237
 
@@ -187,7 +240,10 @@ async def list_directory(path: str, ignore: list[str] | None = None) -> list[str
187
240
  """
188
241
  try:
189
242
  # Validate and resolve the path
190
- dir_path = validate_file_path(path)
243
+ if settings.path_confinement.enabled:
244
+ dir_path = ctx.context.path_validator.validate_path(path, operation="list")
245
+ else:
246
+ dir_path = validate_file_path(path)
191
247
 
192
248
  # Validate path is a directory
193
249
  if not dir_path.is_dir():