janito 2.6.1__py3-none-any.whl → 2.8.0__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.
- janito/__init__.py +6 -7
- janito/__main__.py +4 -5
- janito/_version.py +55 -58
- janito/agent/setup_agent.py +308 -241
- janito/agent/templates/profiles/{system_prompt_template_software developer.txt.j2 → system_prompt_template_Developer_with_Python_Tools.txt.j2} +43 -39
- janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +3 -12
- janito/cli/__init__.py +9 -10
- janito/cli/chat_mode/bindings.py +38 -38
- janito/cli/chat_mode/chat_entry.py +21 -23
- janito/cli/chat_mode/prompt_style.py +22 -25
- janito/cli/chat_mode/script_runner.py +158 -154
- janito/cli/chat_mode/session.py +80 -35
- janito/cli/chat_mode/session_profile_select.py +61 -52
- janito/cli/chat_mode/shell/commands/__init__.py +1 -5
- janito/cli/chat_mode/shell/commands/_priv_check.py +1 -0
- janito/cli/chat_mode/shell/commands/bang.py +10 -3
- janito/cli/chat_mode/shell/commands/conversation_restart.py +24 -7
- janito/cli/chat_mode/shell/commands/execute.py +22 -7
- janito/cli/chat_mode/shell/commands/help.py +4 -1
- janito/cli/chat_mode/shell/commands/model.py +13 -5
- janito/cli/chat_mode/shell/commands/privileges.py +21 -0
- janito/cli/chat_mode/shell/commands/prompt.py +0 -2
- janito/cli/chat_mode/shell/commands/read.py +22 -5
- janito/cli/chat_mode/shell/commands/tools.py +15 -4
- janito/cli/chat_mode/shell/commands/write.py +22 -5
- janito/cli/chat_mode/shell/input_history.py +3 -1
- janito/cli/chat_mode/shell/session/manager.py +0 -2
- janito/cli/chat_mode/toolbar.py +25 -19
- janito/cli/cli_commands/list_models.py +1 -1
- janito/cli/cli_commands/list_providers.py +1 -0
- janito/cli/cli_commands/list_tools.py +35 -7
- janito/cli/cli_commands/model_utils.py +5 -3
- janito/cli/cli_commands/show_config.py +12 -0
- janito/cli/cli_commands/show_system_prompt.py +23 -9
- janito/cli/config.py +0 -13
- janito/cli/core/getters.py +2 -0
- janito/cli/core/runner.py +25 -8
- janito/cli/core/setters.py +13 -76
- janito/cli/main_cli.py +9 -25
- janito/cli/prompt_core.py +19 -18
- janito/cli/prompt_setup.py +6 -3
- janito/cli/rich_terminal_reporter.py +19 -5
- janito/cli/single_shot_mode/handler.py +104 -95
- janito/cli/verbose_output.py +5 -1
- janito/config_manager.py +4 -0
- janito/drivers/azure_openai/driver.py +27 -30
- janito/drivers/driver_registry.py +27 -27
- janito/drivers/openai/driver.py +452 -436
- janito/formatting_token.py +12 -4
- janito/llm/agent.py +15 -6
- janito/llm/driver.py +1 -0
- janito/provider_registry.py +139 -178
- janito/providers/__init__.py +2 -0
- janito/providers/anthropic/model_info.py +40 -41
- janito/providers/anthropic/provider.py +75 -80
- janito/providers/azure_openai/provider.py +9 -4
- janito/providers/deepseek/provider.py +5 -4
- janito/providers/google/model_info.py +4 -2
- janito/providers/google/provider.py +11 -5
- janito/providers/groq/__init__.py +1 -0
- janito/providers/groq/model_info.py +46 -0
- janito/providers/groq/provider.py +76 -0
- janito/providers/moonshotai/__init__.py +1 -0
- janito/providers/moonshotai/model_info.py +15 -0
- janito/providers/moonshotai/provider.py +89 -0
- janito/providers/openai/provider.py +6 -7
- janito/tools/__init__.py +2 -0
- janito/tools/adapters/local/__init__.py +67 -66
- janito/tools/adapters/local/adapter.py +21 -4
- janito/tools/adapters/local/ask_user.py +1 -0
- janito/tools/adapters/local/copy_file.py +1 -0
- janito/tools/adapters/local/create_directory.py +1 -0
- janito/tools/adapters/local/create_file.py +1 -0
- janito/tools/adapters/local/delete_text_in_file.py +2 -1
- janito/tools/adapters/local/fetch_url.py +1 -0
- janito/tools/adapters/local/find_files.py +7 -6
- janito/tools/adapters/local/get_file_outline/core.py +1 -0
- janito/tools/adapters/local/get_file_outline/java_outline.py +22 -15
- janito/tools/adapters/local/get_file_outline/search_outline.py +1 -0
- janito/tools/adapters/local/move_file.py +4 -3
- janito/tools/adapters/local/open_html_in_browser.py +15 -5
- janito/tools/adapters/local/open_url.py +1 -0
- janito/tools/adapters/local/python_code_run.py +1 -0
- janito/tools/adapters/local/python_command_run.py +1 -0
- janito/tools/adapters/local/python_file_run.py +1 -0
- janito/tools/adapters/local/read_files.py +55 -40
- janito/tools/adapters/local/remove_directory.py +1 -0
- janito/tools/adapters/local/remove_file.py +1 -0
- janito/tools/adapters/local/replace_text_in_file.py +4 -3
- janito/tools/adapters/local/run_bash_command.py +1 -0
- janito/tools/adapters/local/run_powershell_command.py +1 -0
- janito/tools/adapters/local/search_text/core.py +18 -17
- janito/tools/adapters/local/search_text/match_lines.py +5 -5
- janito/tools/adapters/local/search_text/pattern_utils.py +1 -1
- janito/tools/adapters/local/search_text/traverse_directory.py +7 -7
- janito/tools/adapters/local/validate_file_syntax/core.py +1 -1
- janito/tools/adapters/local/validate_file_syntax/html_validator.py +8 -1
- janito/tools/disabled_tools.py +68 -0
- janito/tools/path_security.py +18 -11
- janito/tools/permissions.py +6 -0
- janito/tools/permissions_parse.py +4 -3
- janito/tools/tool_base.py +11 -5
- janito/tools/tool_use_tracker.py +1 -4
- janito/tools/tool_utils.py +1 -1
- janito/tools/tools_adapter.py +57 -25
- {janito-2.6.1.dist-info → janito-2.8.0.dist-info}/METADATA +411 -417
- janito-2.8.0.dist-info/RECORD +202 -0
- janito/cli/chat_mode/shell/commands/livelogs.py +0 -49
- janito/drivers/mistralai/driver.py +0 -41
- janito/providers/mistralai/model_info.py +0 -37
- janito/providers/mistralai/provider.py +0 -72
- janito/providers/provider_static_info.py +0 -18
- janito-2.6.1.dist-info/RECORD +0 -199
- /janito/agent/templates/profiles/{system_prompt_template_assistant.txt.j2 → system_prompt_template_model_conversation_without_tools_or_context.txt.j2} +0 -0
- {janito-2.6.1.dist-info → janito-2.8.0.dist-info}/WHEEL +0 -0
- {janito-2.6.1.dist-info → janito-2.8.0.dist-info}/entry_points.txt +0 -0
- {janito-2.6.1.dist-info → janito-2.8.0.dist-info}/licenses/LICENSE +0 -0
- {janito-2.6.1.dist-info → janito-2.8.0.dist-info}/top_level.txt +0 -0
@@ -15,11 +15,11 @@ from janito.tools.adapters.local.adapter import register_local_tool as register_
|
|
15
15
|
@register_tool
|
16
16
|
class SearchTextTool(ToolBase):
|
17
17
|
"""
|
18
|
-
Search for a text
|
18
|
+
Search for a text query in all files within one or more directories or file paths and return matching lines or counts. Respects .gitignore.
|
19
19
|
Args:
|
20
20
|
paths (str): String of one or more paths (space-separated) to search in. Each path can be a directory or a file.
|
21
|
-
|
22
|
-
|
21
|
+
query (str): Text or regular expression to search for in files. Must not be empty. When use_regex=True, this is treated as a regex pattern; otherwise as plain text.
|
22
|
+
use_regex (bool): If True, treat query as a regular expression. If False, treat as plain text (default).
|
23
23
|
case_sensitive (bool): If False, perform a case-insensitive search. Default is True (case sensitive).
|
24
24
|
max_depth (int, optional): Maximum directory depth to search. If 0 (default), search is recursive with no depth limit. If >0, limits recursion to that depth. Setting max_depth=1 disables recursion (only top-level directory). Ignored for file paths.
|
25
25
|
max_results (int, optional): Maximum number of results to return. Defaults to 100. 0 means no limit.
|
@@ -29,13 +29,14 @@ class SearchTextTool(ToolBase):
|
|
29
29
|
If count_only is True, returns per-file and total match counts.
|
30
30
|
If max_results is reached, appends a note to the output.
|
31
31
|
"""
|
32
|
+
|
32
33
|
permissions = ToolPermissions(read=True)
|
33
34
|
tool_name = "search_text"
|
34
35
|
|
35
36
|
def _handle_file(
|
36
37
|
self,
|
37
38
|
search_path,
|
38
|
-
|
39
|
+
query,
|
39
40
|
regex,
|
40
41
|
use_regex,
|
41
42
|
case_sensitive,
|
@@ -46,7 +47,7 @@ class SearchTextTool(ToolBase):
|
|
46
47
|
if count_only:
|
47
48
|
match_count, dir_limit_reached, _ = read_file_lines(
|
48
49
|
search_path,
|
49
|
-
|
50
|
+
query,
|
50
51
|
regex,
|
51
52
|
use_regex,
|
52
53
|
case_sensitive,
|
@@ -59,7 +60,7 @@ class SearchTextTool(ToolBase):
|
|
59
60
|
else:
|
60
61
|
dir_output, dir_limit_reached, match_count_list = read_file_lines(
|
61
62
|
search_path,
|
62
|
-
|
63
|
+
query,
|
63
64
|
regex,
|
64
65
|
use_regex,
|
65
66
|
case_sensitive,
|
@@ -77,7 +78,7 @@ class SearchTextTool(ToolBase):
|
|
77
78
|
def _handle_path(
|
78
79
|
self,
|
79
80
|
search_path,
|
80
|
-
|
81
|
+
query,
|
81
82
|
regex,
|
82
83
|
use_regex,
|
83
84
|
case_sensitive,
|
@@ -87,9 +88,9 @@ class SearchTextTool(ToolBase):
|
|
87
88
|
count_only,
|
88
89
|
):
|
89
90
|
info_str = tr(
|
90
|
-
"🔍 Search {search_type} '{
|
91
|
+
"🔍 Search {search_type} '{query}' in '{disp_path}'",
|
91
92
|
search_type=("regex" if use_regex else "text"),
|
92
|
-
|
93
|
+
query=query,
|
93
94
|
disp_path=display_path(search_path),
|
94
95
|
)
|
95
96
|
if max_depth > 0:
|
@@ -100,7 +101,7 @@ class SearchTextTool(ToolBase):
|
|
100
101
|
if os.path.isfile(search_path):
|
101
102
|
dir_output, dir_limit_reached, per_file_counts = self._handle_file(
|
102
103
|
search_path,
|
103
|
-
|
104
|
+
query,
|
104
105
|
regex,
|
105
106
|
use_regex,
|
106
107
|
case_sensitive,
|
@@ -112,7 +113,7 @@ class SearchTextTool(ToolBase):
|
|
112
113
|
if count_only:
|
113
114
|
per_file_counts, dir_limit_reached, _ = traverse_directory(
|
114
115
|
search_path,
|
115
|
-
|
116
|
+
query,
|
116
117
|
regex,
|
117
118
|
use_regex,
|
118
119
|
case_sensitive,
|
@@ -125,7 +126,7 @@ class SearchTextTool(ToolBase):
|
|
125
126
|
else:
|
126
127
|
dir_output, dir_limit_reached, per_file_counts = traverse_directory(
|
127
128
|
search_path,
|
128
|
-
|
129
|
+
query,
|
129
130
|
regex,
|
130
131
|
use_regex,
|
131
132
|
case_sensitive,
|
@@ -154,15 +155,15 @@ class SearchTextTool(ToolBase):
|
|
154
155
|
def run(
|
155
156
|
self,
|
156
157
|
paths: str,
|
157
|
-
|
158
|
-
|
158
|
+
query: str,
|
159
|
+
use_regex: bool = False,
|
159
160
|
case_sensitive: bool = False,
|
160
161
|
max_depth: int = 0,
|
161
162
|
max_results: int = 100,
|
162
163
|
count_only: bool = False,
|
163
164
|
) -> str:
|
164
165
|
regex, use_regex, error_msg = prepare_pattern(
|
165
|
-
|
166
|
+
query, use_regex, case_sensitive, self.report_error, self.report_warning
|
166
167
|
)
|
167
168
|
if error_msg:
|
168
169
|
return error_msg
|
@@ -173,7 +174,7 @@ class SearchTextTool(ToolBase):
|
|
173
174
|
info_str, dir_output, dir_limit_reached, per_file_counts = (
|
174
175
|
self._handle_path(
|
175
176
|
search_path,
|
176
|
-
|
177
|
+
query,
|
177
178
|
regex,
|
178
179
|
use_regex,
|
179
180
|
case_sensitive,
|
@@ -186,7 +187,7 @@ class SearchTextTool(ToolBase):
|
|
186
187
|
if count_only:
|
187
188
|
all_per_file_counts.extend(per_file_counts)
|
188
189
|
result_str = format_result(
|
189
|
-
|
190
|
+
query,
|
190
191
|
use_regex,
|
191
192
|
dir_output,
|
192
193
|
dir_limit_reached,
|
@@ -20,12 +20,12 @@ def is_binary_file(path, blocksize=1024):
|
|
20
20
|
return False
|
21
21
|
|
22
22
|
|
23
|
-
def match_line(line,
|
23
|
+
def match_line(line, query, regex, use_regex, case_sensitive):
|
24
24
|
if use_regex:
|
25
25
|
return regex and regex.search(line)
|
26
26
|
if not case_sensitive:
|
27
|
-
return
|
28
|
-
return
|
27
|
+
return query.lower() in line.lower()
|
28
|
+
return query in line
|
29
29
|
|
30
30
|
|
31
31
|
def should_limit(max_results, total_results, match_count, count_only, dir_output):
|
@@ -37,7 +37,7 @@ def should_limit(max_results, total_results, match_count, count_only, dir_output
|
|
37
37
|
|
38
38
|
def read_file_lines(
|
39
39
|
path,
|
40
|
-
|
40
|
+
query,
|
41
41
|
regex,
|
42
42
|
use_regex,
|
43
43
|
case_sensitive,
|
@@ -53,7 +53,7 @@ def read_file_lines(
|
|
53
53
|
open_kwargs = {"mode": "r", "encoding": "utf-8"}
|
54
54
|
with open(path, **open_kwargs) as f:
|
55
55
|
for lineno, line in enumerate(f, 1):
|
56
|
-
if match_line(line,
|
56
|
+
if match_line(line, query, regex, use_regex, case_sensitive):
|
57
57
|
match_count += 1
|
58
58
|
if not count_only:
|
59
59
|
dir_output.append(f"{path}:{lineno}: {line.rstrip()}")
|
@@ -42,7 +42,7 @@ def prepare_pattern(pattern, is_regex, case_sensitive, report_error, report_warn
|
|
42
42
|
|
43
43
|
|
44
44
|
def format_result(
|
45
|
-
|
45
|
+
query, use_regex, output, limit_reached, count_only=False, per_file_counts=None
|
46
46
|
):
|
47
47
|
# Ensure output is always a list for joining
|
48
48
|
if output is None or not isinstance(output, (list, tuple)):
|
@@ -26,7 +26,7 @@ def filter_dirs(dirs, root, gitignore_filter):
|
|
26
26
|
def process_file_count_only(
|
27
27
|
path,
|
28
28
|
per_file_counts,
|
29
|
-
|
29
|
+
query,
|
30
30
|
regex,
|
31
31
|
use_regex,
|
32
32
|
case_sensitive,
|
@@ -35,7 +35,7 @@ def process_file_count_only(
|
|
35
35
|
):
|
36
36
|
match_count, file_limit_reached, _ = read_file_lines(
|
37
37
|
path,
|
38
|
-
|
38
|
+
query,
|
39
39
|
regex,
|
40
40
|
use_regex,
|
41
41
|
case_sensitive,
|
@@ -52,7 +52,7 @@ def process_file_collect(
|
|
52
52
|
path,
|
53
53
|
dir_output,
|
54
54
|
per_file_counts,
|
55
|
-
|
55
|
+
query,
|
56
56
|
regex,
|
57
57
|
use_regex,
|
58
58
|
case_sensitive,
|
@@ -61,7 +61,7 @@ def process_file_collect(
|
|
61
61
|
):
|
62
62
|
actual_match_count, file_limit_reached, file_lines_output = read_file_lines(
|
63
63
|
path,
|
64
|
-
|
64
|
+
query,
|
65
65
|
regex,
|
66
66
|
use_regex,
|
67
67
|
case_sensitive,
|
@@ -86,7 +86,7 @@ def should_limit_depth(root, search_path, max_depth, dirs):
|
|
86
86
|
|
87
87
|
def traverse_directory(
|
88
88
|
search_path,
|
89
|
-
|
89
|
+
query,
|
90
90
|
regex,
|
91
91
|
use_regex,
|
92
92
|
case_sensitive,
|
@@ -111,7 +111,7 @@ def traverse_directory(
|
|
111
111
|
file_limit_reached = process_file_count_only(
|
112
112
|
path,
|
113
113
|
per_file_counts,
|
114
|
-
|
114
|
+
query,
|
115
115
|
regex,
|
116
116
|
use_regex,
|
117
117
|
case_sensitive,
|
@@ -126,7 +126,7 @@ def traverse_directory(
|
|
126
126
|
path,
|
127
127
|
dir_output,
|
128
128
|
per_file_counts,
|
129
|
-
|
129
|
+
query,
|
130
130
|
regex,
|
131
131
|
use_regex,
|
132
132
|
case_sensitive,
|
@@ -82,6 +82,7 @@ class ValidateFileSyntaxTool(ToolBase):
|
|
82
82
|
- "⚠️ Warning: Syntax error: <error message>"
|
83
83
|
- "⚠️ Warning: Unsupported file extension: <ext>"
|
84
84
|
"""
|
85
|
+
|
85
86
|
permissions = ToolPermissions(read=True)
|
86
87
|
tool_name = "validate_file_syntax"
|
87
88
|
|
@@ -96,7 +97,6 @@ class ValidateFileSyntaxTool(ToolBase):
|
|
96
97
|
)
|
97
98
|
result = validate_file_syntax(
|
98
99
|
path,
|
99
|
-
|
100
100
|
report_warning=self.report_warning,
|
101
101
|
report_success=self.report_success,
|
102
102
|
)
|
@@ -1,6 +1,10 @@
|
|
1
1
|
from janito.i18n import tr
|
2
2
|
import re
|
3
|
-
|
3
|
+
|
4
|
+
try:
|
5
|
+
from lxml import etree
|
6
|
+
except ImportError:
|
7
|
+
etree = None
|
4
8
|
|
5
9
|
|
6
10
|
def validate_html(path: str) -> str:
|
@@ -48,6 +52,9 @@ def _find_js_outside_script(html_content):
|
|
48
52
|
|
49
53
|
def _parse_html_and_collect_errors(path):
|
50
54
|
lxml_error = None
|
55
|
+
if etree is None:
|
56
|
+
lxml_error = tr("⚠️ lxml not installed. Cannot validate HTML.")
|
57
|
+
return lxml_error
|
51
58
|
try:
|
52
59
|
parser = etree.HTMLParser(recover=False)
|
53
60
|
with open(path, "rb") as f:
|
@@ -0,0 +1,68 @@
|
|
1
|
+
"""Management of disabled tools configuration."""
|
2
|
+
|
3
|
+
|
4
|
+
class DisabledToolsState:
|
5
|
+
"""Singleton to manage disabled tools configuration."""
|
6
|
+
|
7
|
+
_instance = None
|
8
|
+
_disabled_tools = set()
|
9
|
+
|
10
|
+
def __new__(cls):
|
11
|
+
if cls._instance is None:
|
12
|
+
cls._instance = super().__new__(cls)
|
13
|
+
return cls._instance
|
14
|
+
|
15
|
+
@classmethod
|
16
|
+
def get_disabled_tools(cls):
|
17
|
+
"""Get the set of disabled tool names."""
|
18
|
+
return cls._disabled_tools.copy()
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def set_disabled_tools(cls, tool_names):
|
22
|
+
"""Set the disabled tools from a list or set of tool names."""
|
23
|
+
if isinstance(tool_names, str):
|
24
|
+
tool_names = [
|
25
|
+
name.strip() for name in tool_names.split(",") if name.strip()
|
26
|
+
]
|
27
|
+
cls._disabled_tools = set(tool_names)
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def is_tool_disabled(cls, tool_name):
|
31
|
+
"""Check if a specific tool is disabled."""
|
32
|
+
return tool_name in cls._disabled_tools
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def disable_tool(cls, tool_name):
|
36
|
+
"""Add a tool to the disabled list."""
|
37
|
+
cls._disabled_tools.add(tool_name)
|
38
|
+
|
39
|
+
@classmethod
|
40
|
+
def enable_tool(cls, tool_name):
|
41
|
+
"""Remove a tool from the disabled list."""
|
42
|
+
cls._disabled_tools.discard(tool_name)
|
43
|
+
|
44
|
+
|
45
|
+
# Convenience functions
|
46
|
+
def get_disabled_tools():
|
47
|
+
"""Get the current set of disabled tools."""
|
48
|
+
return DisabledToolsState.get_disabled_tools()
|
49
|
+
|
50
|
+
|
51
|
+
def set_disabled_tools(tool_names):
|
52
|
+
"""Set the disabled tools from a list, set, or comma-separated string."""
|
53
|
+
DisabledToolsState.set_disabled_tools(tool_names)
|
54
|
+
|
55
|
+
|
56
|
+
def is_tool_disabled(tool_name):
|
57
|
+
"""Check if a specific tool is disabled."""
|
58
|
+
return DisabledToolsState.is_tool_disabled(tool_name)
|
59
|
+
|
60
|
+
|
61
|
+
def load_disabled_tools_from_config():
|
62
|
+
"""Load disabled tools from global config."""
|
63
|
+
from janito.config import config
|
64
|
+
|
65
|
+
disabled_str = config.get("disabled_tools", "")
|
66
|
+
if disabled_str:
|
67
|
+
DisabledToolsState.set_disabled_tools(disabled_str)
|
68
|
+
return DisabledToolsState.get_disabled_tools()
|
janito/tools/path_security.py
CHANGED
@@ -20,6 +20,7 @@ Public interface
|
|
20
20
|
Both helpers raise :class:`PathSecurityError` if a path tries to escape the
|
21
21
|
workspace.
|
22
22
|
"""
|
23
|
+
|
23
24
|
from __future__ import annotations
|
24
25
|
|
25
26
|
import os
|
@@ -46,7 +47,11 @@ class PathSecurityError(Exception):
|
|
46
47
|
# ---------------------------------------------------------------------------
|
47
48
|
|
48
49
|
|
49
|
-
def is_path_within_workdir(
|
50
|
+
def is_path_within_workdir(
|
51
|
+
path: str, workdir: str | None
|
52
|
+
) -> (
|
53
|
+
bool
|
54
|
+
): # noqa: D401 – we start with an imperative verb # noqa: D401 – we start with an imperative verb
|
50
55
|
"""Return *True* if *path* is located inside *workdir* (or equals it).
|
51
56
|
|
52
57
|
Relative *path*s are **resolved relative to the *workdir***, *not* to the
|
@@ -84,6 +89,7 @@ def is_path_within_workdir(path: str, workdir: str | None) -> bool: # noqa: D40
|
|
84
89
|
|
85
90
|
# Additionally allow files located inside the system temporary directory.
|
86
91
|
import tempfile
|
92
|
+
|
87
93
|
abs_tempdir = os.path.abspath(tempfile.gettempdir())
|
88
94
|
try:
|
89
95
|
common_temp = os.path.commonpath([abs_tempdir, abs_path])
|
@@ -129,20 +135,18 @@ def _extract_path_keys_from_schema(schema: Mapping[str, Any]) -> set[str]:
|
|
129
135
|
path_keys: set[str] = set()
|
130
136
|
if schema is not None:
|
131
137
|
for k, v in schema.get("properties", {}).items():
|
132
|
-
if (
|
133
|
-
v.get("
|
134
|
-
|
135
|
-
v.get("
|
136
|
-
|
137
|
-
|
138
|
-
or k.endswith("path")
|
139
|
-
or k == "path"
|
140
|
-
)
|
138
|
+
if v.get("format") == "path" or (
|
139
|
+
v.get("type") == "string"
|
140
|
+
and (
|
141
|
+
"path" in v.get("description", "").lower()
|
142
|
+
or k.endswith("path")
|
143
|
+
or k == "path"
|
141
144
|
)
|
142
145
|
):
|
143
146
|
path_keys.add(k)
|
144
147
|
return path_keys
|
145
148
|
|
149
|
+
|
146
150
|
def _validate_argument_value(key: str, value: Any, workdir: str) -> None:
|
147
151
|
"""Validate a single argument value (string or list of strings) for path security."""
|
148
152
|
# Single string argument → validate directly.
|
@@ -156,6 +160,7 @@ def _validate_argument_value(key: str, value: Any, workdir: str) -> None:
|
|
156
160
|
if not is_path_within_workdir(item, workdir):
|
157
161
|
_raise_outside_workspace_error(key, item, workdir)
|
158
162
|
|
163
|
+
|
159
164
|
def validate_paths_in_arguments(
|
160
165
|
arguments: Mapping[str, Any] | None,
|
161
166
|
workdir: str | None,
|
@@ -190,7 +195,9 @@ def validate_paths_in_arguments(
|
|
190
195
|
# ---------------------------------------------------------------------------
|
191
196
|
|
192
197
|
|
193
|
-
def _raise_outside_workspace_error(
|
198
|
+
def _raise_outside_workspace_error(
|
199
|
+
key: str, path: str, workdir: str
|
200
|
+
) -> None: # noqa: D401
|
194
201
|
"""Raise a consistent :class:`PathSecurityError` for *path*."""
|
195
202
|
abs_workdir = os.path.abspath(workdir)
|
196
203
|
attempted = (
|
janito/tools/permissions.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from janito.tools.tool_base import ToolPermissions
|
2
2
|
|
3
|
+
|
3
4
|
class AllowedPermissionsState:
|
4
5
|
_instance = None
|
5
6
|
_permissions = ToolPermissions(read=False, write=False, execute=False)
|
@@ -30,16 +31,21 @@ class AllowedPermissionsState:
|
|
30
31
|
def get_default_permissions(cls):
|
31
32
|
return cls._default_permissions
|
32
33
|
|
34
|
+
|
33
35
|
# Convenience functions
|
34
36
|
|
37
|
+
|
35
38
|
def get_global_allowed_permissions():
|
36
39
|
return AllowedPermissionsState.get_permissions()
|
37
40
|
|
41
|
+
|
38
42
|
def set_global_allowed_permissions(permissions):
|
39
43
|
AllowedPermissionsState.set_permissions(permissions)
|
40
44
|
|
45
|
+
|
41
46
|
def set_default_allowed_permissions(permissions):
|
42
47
|
AllowedPermissionsState.set_default_permissions(permissions)
|
43
48
|
|
49
|
+
|
44
50
|
def get_default_allowed_permissions():
|
45
51
|
return AllowedPermissionsState.get_default_permissions()
|
@@ -1,12 +1,13 @@
|
|
1
1
|
from janito.tools.tool_base import ToolPermissions
|
2
2
|
|
3
|
+
|
3
4
|
def parse_permissions_string(perm_str: str) -> ToolPermissions:
|
4
5
|
"""
|
5
6
|
Parse a string like 'rwx', 'rw', 'r', etc. into a ToolPermissions object.
|
6
7
|
"""
|
7
8
|
perm_str = perm_str.lower()
|
8
9
|
return ToolPermissions(
|
9
|
-
read=
|
10
|
-
write=
|
11
|
-
execute=
|
10
|
+
read="r" in perm_str,
|
11
|
+
write="w" in perm_str,
|
12
|
+
execute="x" in perm_str,
|
12
13
|
)
|
janito/tools/tool_base.py
CHANGED
@@ -4,8 +4,10 @@ from janito.event_bus.bus import event_bus as default_event_bus
|
|
4
4
|
|
5
5
|
from collections import namedtuple
|
6
6
|
|
7
|
-
|
7
|
+
|
8
|
+
class ToolPermissions(namedtuple("ToolPermissions", ["read", "write", "execute"])):
|
8
9
|
__slots__ = ()
|
10
|
+
|
9
11
|
def __new__(cls, read=False, write=False, execute=False):
|
10
12
|
return super().__new__(cls, read, write, execute)
|
11
13
|
|
@@ -18,11 +20,16 @@ class ToolBase:
|
|
18
20
|
Base class for all tools in the janito project.
|
19
21
|
Extend this class to implement specific tool functionality.
|
20
22
|
"""
|
21
|
-
|
23
|
+
|
24
|
+
permissions: "ToolPermissions" = None # Required: must be set by subclasses
|
22
25
|
|
23
26
|
def __init__(self, name=None, event_bus=None):
|
24
|
-
if self.permissions is None or not isinstance(
|
25
|
-
|
27
|
+
if self.permissions is None or not isinstance(
|
28
|
+
self.permissions, ToolPermissions
|
29
|
+
):
|
30
|
+
raise ValueError(
|
31
|
+
f"Tool '{self.__class__.__name__}' must define a 'permissions' attribute of type ToolPermissions."
|
32
|
+
)
|
26
33
|
self.name = name or self.__class__.__name__
|
27
34
|
self._event_bus = event_bus or default_event_bus
|
28
35
|
|
@@ -48,7 +55,6 @@ class ToolBase:
|
|
48
55
|
)
|
49
56
|
)
|
50
57
|
|
51
|
-
|
52
58
|
def report_error(self, message: str, context: dict = None):
|
53
59
|
self._event_bus.publish(
|
54
60
|
ReportEvent(
|
janito/tools/tool_use_tracker.py
CHANGED
@@ -50,10 +50,7 @@ class ToolUseTracker:
|
|
50
50
|
for entry in self._history:
|
51
51
|
if entry["tool"] == "view_file":
|
52
52
|
params = entry["params"]
|
53
|
-
if (
|
54
|
-
"path" in params
|
55
|
-
and normalize_path(params["path"]) == norm_path
|
56
|
-
):
|
53
|
+
if "path" in params and normalize_path(params["path"]) == norm_path:
|
57
54
|
# If both from_line and to_line are None, full file was read
|
58
55
|
if (
|
59
56
|
params.get("from_line") is None
|