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.
- vibecore/agents/default.py +3 -3
- vibecore/agents/task.py +3 -3
- vibecore/cli.py +67 -43
- vibecore/context.py +74 -11
- vibecore/flow.py +335 -73
- vibecore/handlers/stream_handler.py +35 -56
- vibecore/main.py +70 -272
- vibecore/session/jsonl_session.py +3 -1
- vibecore/session/loader.py +2 -2
- vibecore/settings.py +48 -1
- vibecore/tools/file/executor.py +59 -13
- vibecore/tools/file/tools.py +9 -9
- vibecore/tools/path_validator.py +251 -0
- vibecore/tools/python/helpers.py +2 -2
- vibecore/tools/python/tools.py +2 -2
- vibecore/tools/shell/executor.py +63 -7
- vibecore/tools/shell/tools.py +9 -9
- vibecore/tools/task/executor.py +2 -2
- vibecore/tools/task/tools.py +2 -2
- vibecore/tools/todo/manager.py +2 -10
- vibecore/tools/todo/models.py +5 -14
- vibecore/tools/todo/tools.py +5 -5
- vibecore/tools/webfetch/tools.py +1 -4
- vibecore/tools/websearch/ddgs/backend.py +1 -1
- vibecore/tools/websearch/tools.py +1 -4
- vibecore/widgets/core.py +3 -17
- vibecore/widgets/feedback.py +164 -0
- vibecore/widgets/feedback.tcss +121 -0
- vibecore/widgets/messages.py +22 -2
- vibecore/widgets/messages.tcss +28 -0
- vibecore/widgets/tool_messages.py +19 -4
- vibecore/widgets/tool_messages.tcss +23 -0
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/METADATA +122 -29
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/RECORD +37 -34
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/WHEEL +0 -0
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/entry_points.txt +0 -0
- {vibecore-0.3.0.dist-info → vibecore-0.6.2.dist-info}/licenses/LICENSE +0 -0
vibecore/tools/file/executor.py
CHANGED
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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():
|
vibecore/tools/file/tools.py
CHANGED
|
@@ -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
|
|
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[
|
|
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[
|
|
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[
|
|
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[
|
|
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()
|
vibecore/tools/python/helpers.py
CHANGED
|
@@ -5,7 +5,7 @@ from io import BytesIO
|
|
|
5
5
|
|
|
6
6
|
from agents import RunContextWrapper
|
|
7
7
|
|
|
8
|
-
from vibecore.context import
|
|
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[
|
|
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.
|
vibecore/tools/python/tools.py
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from agents import RunContextWrapper, function_tool
|
|
4
4
|
|
|
5
|
-
from vibecore.context import
|
|
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[
|
|
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:
|
vibecore/tools/shell/executor.py
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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():
|