janito 2.22.0__py3-none-any.whl → 2.24.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/README.md +0 -0
- janito/agent/setup_agent.py +14 -0
- janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +59 -11
- janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +53 -7
- janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +108 -8
- janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +53 -1
- janito/cli/chat_mode/session.py +8 -1
- janito/cli/chat_mode/shell/commands/__init__.py +2 -0
- janito/cli/chat_mode/shell/commands/security/__init__.py +1 -0
- janito/cli/chat_mode/shell/commands/security/allowed_sites.py +94 -0
- janito/cli/chat_mode/shell/commands/security_command.py +51 -0
- janito/cli/chat_mode/shell/commands.bak.zip +0 -0
- janito/cli/chat_mode/shell/session.bak.zip +0 -0
- janito/cli/cli_commands/list_plugins.py +45 -0
- janito/cli/cli_commands/show_system_prompt.py +13 -40
- janito/cli/core/getters.py +4 -0
- janito/cli/core/runner.py +7 -2
- janito/cli/core/setters.py +10 -1
- janito/cli/main_cli.py +25 -3
- janito/cli/single_shot_mode/handler.py +3 -1
- janito/config_manager.py +10 -0
- janito/docs/GETTING_STARTED.md +0 -0
- janito/drivers/dashscope.bak.zip +0 -0
- janito/drivers/openai/README.md +0 -0
- janito/drivers/openai_responses.bak.zip +0 -0
- janito/llm/README.md +0 -0
- janito/mkdocs.yml +0 -0
- janito/plugins/__init__.py +17 -0
- janito/plugins/base.py +93 -0
- janito/plugins/discovery.py +160 -0
- janito/plugins/manager.py +185 -0
- janito/providers/dashscope.bak.zip +0 -0
- janito/providers/ibm/README.md +0 -0
- janito/shell.bak.zip +0 -0
- janito/tools/DOCSTRING_STANDARD.txt +0 -0
- janito/tools/README.md +0 -0
- janito/tools/adapters/local/__init__.py +2 -0
- janito/tools/adapters/local/adapter.py +55 -0
- janito/tools/adapters/local/ask_user.py +2 -0
- janito/tools/adapters/local/fetch_url.py +89 -4
- janito/tools/adapters/local/find_files.py +2 -0
- janito/tools/adapters/local/get_file_outline/core.py +2 -0
- janito/tools/adapters/local/get_file_outline/search_outline.py +2 -0
- janito/tools/adapters/local/open_html_in_browser.py +2 -0
- janito/tools/adapters/local/open_url.py +2 -0
- janito/tools/adapters/local/python_code_run.py +15 -10
- janito/tools/adapters/local/python_command_run.py +14 -9
- janito/tools/adapters/local/python_file_run.py +15 -10
- janito/tools/adapters/local/read_chart.py +252 -0
- janito/tools/adapters/local/read_files.py +2 -0
- janito/tools/adapters/local/replace_text_in_file.py +1 -1
- janito/tools/adapters/local/run_bash_command.py +18 -12
- janito/tools/adapters/local/run_powershell_command.py +15 -9
- janito/tools/adapters/local/search_text/core.py +2 -0
- janito/tools/adapters/local/validate_file_syntax/core.py +6 -0
- janito/tools/adapters/local/validate_file_syntax/jinja2_validator.py +47 -0
- janito/tools/adapters/local/view_file.py +2 -0
- janito/tools/loop_protection.py +115 -0
- janito/tools/loop_protection_decorator.py +110 -0
- janito/tools/outline_file.bak.zip +0 -0
- janito/tools/url_whitelist.py +121 -0
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/METADATA +411 -411
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/RECORD +52 -39
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/entry_points.txt +0 -0
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/licenses/LICENSE +0 -0
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/top_level.txt +0 -0
- {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
import functools
|
2
|
+
import time
|
3
|
+
import threading
|
4
|
+
from typing import Callable, Any
|
5
|
+
from janito.tools.loop_protection import LoopProtection
|
6
|
+
from janito.tools.tool_use_tracker import normalize_path
|
7
|
+
|
8
|
+
|
9
|
+
# Global tracking for decorator-based loop protection
|
10
|
+
_decorator_call_tracker = {}
|
11
|
+
_decorator_call_tracker_lock = threading.Lock()
|
12
|
+
|
13
|
+
|
14
|
+
def protect_against_loops(
|
15
|
+
max_calls: int = 5,
|
16
|
+
time_window: float = 10.0
|
17
|
+
):
|
18
|
+
"""
|
19
|
+
Decorator that adds loop protection to tool run methods.
|
20
|
+
|
21
|
+
This decorator monitors tool executions and prevents excessive calls within
|
22
|
+
a configurable time window. It helps prevent infinite loops or excessive
|
23
|
+
resource consumption when tools are called repeatedly.
|
24
|
+
|
25
|
+
When the configured limits are exceeded, the decorator raises a RuntimeError
|
26
|
+
with a descriptive message. This exception will propagate up the call stack
|
27
|
+
unless caught by a try/except block in the calling code.
|
28
|
+
|
29
|
+
The decorator works by:
|
30
|
+
1. Tracking the number of calls to the decorated function
|
31
|
+
2. Checking if the calls exceed the configured limits
|
32
|
+
3. Raising a RuntimeError if a potential loop is detected
|
33
|
+
4. Allowing the method to proceed normally if the operation is safe
|
34
|
+
|
35
|
+
Args:
|
36
|
+
max_calls (int): Maximum number of calls allowed within the time window.
|
37
|
+
Defaults to 5 calls.
|
38
|
+
time_window (float): Time window in seconds for detecting excessive calls.
|
39
|
+
Defaults to 10.0 seconds.
|
40
|
+
|
41
|
+
Example:
|
42
|
+
>>> @protect_against_loops(max_calls=3, time_window=5.0)
|
43
|
+
>>> def run(self, path: str) -> str:
|
44
|
+
>>> # Implementation here
|
45
|
+
>>> pass
|
46
|
+
|
47
|
+
>>> @protect_against_loops(max_calls=10, time_window=30.0)
|
48
|
+
>>> def run(self, file_paths: list) -> str:
|
49
|
+
>>> # Implementation here
|
50
|
+
>>> pass
|
51
|
+
|
52
|
+
Note:
|
53
|
+
When loop protection is triggered, a RuntimeError will be raised with a
|
54
|
+
descriptive message. This exception will propagate up the call stack
|
55
|
+
unless caught by a try/except block in the calling code.
|
56
|
+
"""
|
57
|
+
def decorator(func):
|
58
|
+
@functools.wraps(func)
|
59
|
+
def wrapper(*args, **kwargs):
|
60
|
+
# Get the tool instance (self)
|
61
|
+
if not args:
|
62
|
+
# This shouldn't happen in normal usage as methods need self
|
63
|
+
return func(*args, **kwargs)
|
64
|
+
|
65
|
+
# Use the function name as the operation name
|
66
|
+
op_name = func.__name__
|
67
|
+
|
68
|
+
# Check call limits
|
69
|
+
current_time = time.time()
|
70
|
+
|
71
|
+
with _decorator_call_tracker_lock:
|
72
|
+
# Clean up old entries outside the time window
|
73
|
+
if op_name in _decorator_call_tracker:
|
74
|
+
_decorator_call_tracker[op_name] = [
|
75
|
+
timestamp for timestamp in _decorator_call_tracker[op_name]
|
76
|
+
if current_time - timestamp <= time_window
|
77
|
+
]
|
78
|
+
|
79
|
+
# Check if we're exceeding the limit
|
80
|
+
if op_name in _decorator_call_tracker:
|
81
|
+
if len(_decorator_call_tracker[op_name]) >= max_calls:
|
82
|
+
# Check if all recent calls are within the time window
|
83
|
+
if all(current_time - timestamp <= time_window
|
84
|
+
for timestamp in _decorator_call_tracker[op_name]):
|
85
|
+
# Define the error reporting function
|
86
|
+
def _report_error_and_raise(args, operation_type):
|
87
|
+
# Get the tool instance to access report_error method if available
|
88
|
+
tool_instance = args[0] if args else None
|
89
|
+
error_msg = f"Loop protection: Too many {operation_type} operations in a short time period ({max_calls} calls in {time_window}s)"
|
90
|
+
|
91
|
+
# Try to report the error through the tool's reporting mechanism
|
92
|
+
if hasattr(tool_instance, 'report_error'):
|
93
|
+
try:
|
94
|
+
tool_instance.report_error(error_msg)
|
95
|
+
except Exception:
|
96
|
+
pass # If reporting fails, we still raise the error
|
97
|
+
|
98
|
+
raise RuntimeError(error_msg)
|
99
|
+
|
100
|
+
_report_error_and_raise(args, op_name)
|
101
|
+
|
102
|
+
# Record this call
|
103
|
+
if op_name not in _decorator_call_tracker:
|
104
|
+
_decorator_call_tracker[op_name] = []
|
105
|
+
_decorator_call_tracker[op_name].append(current_time)
|
106
|
+
|
107
|
+
# Proceed with the original function
|
108
|
+
return func(*args, **kwargs)
|
109
|
+
return wrapper
|
110
|
+
return decorator
|
File without changes
|
@@ -0,0 +1,121 @@
|
|
1
|
+
"""URL whitelist management for fetch_url tool."""
|
2
|
+
|
3
|
+
import json
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Set, List, Optional
|
6
|
+
from urllib.parse import urlparse
|
7
|
+
|
8
|
+
|
9
|
+
class UrlWhitelistManager:
|
10
|
+
"""Manages allowed sites for the fetch_url tool."""
|
11
|
+
|
12
|
+
def __init__(self):
|
13
|
+
self.config_path = Path.home() / ".janito" / "url_whitelist.json"
|
14
|
+
self._allowed_sites = self._load_whitelist()
|
15
|
+
self._unrestricted_mode = False
|
16
|
+
|
17
|
+
def set_unrestricted_mode(self, enabled: bool = True):
|
18
|
+
"""Enable or disable unrestricted mode (bypasses whitelist)."""
|
19
|
+
self._unrestricted_mode = enabled
|
20
|
+
|
21
|
+
def _load_whitelist(self) -> Set[str]:
|
22
|
+
"""Load the whitelist from config file."""
|
23
|
+
if not self.config_path.exists():
|
24
|
+
return set()
|
25
|
+
|
26
|
+
try:
|
27
|
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
28
|
+
data = json.load(f)
|
29
|
+
return set(data.get('allowed_sites', []))
|
30
|
+
except (json.JSONDecodeError, IOError):
|
31
|
+
return set()
|
32
|
+
|
33
|
+
def _save_whitelist(self):
|
34
|
+
"""Save the whitelist to config file."""
|
35
|
+
self.config_path.parent.mkdir(parents=True, exist_ok=True)
|
36
|
+
try:
|
37
|
+
with open(self.config_path, 'w', encoding='utf-8') as f:
|
38
|
+
json.dump({'allowed_sites': list(self._allowed_sites)}, f, indent=2)
|
39
|
+
except IOError:
|
40
|
+
pass # Silently fail if we can't write
|
41
|
+
|
42
|
+
def is_url_allowed(self, url: str) -> bool:
|
43
|
+
"""Check if a URL is allowed based on the whitelist."""
|
44
|
+
if self._unrestricted_mode:
|
45
|
+
return True # Unrestricted mode bypasses all whitelist checks
|
46
|
+
|
47
|
+
if not self._allowed_sites:
|
48
|
+
return True # No whitelist means all sites allowed
|
49
|
+
|
50
|
+
try:
|
51
|
+
parsed = urlparse(url)
|
52
|
+
domain = parsed.netloc.lower()
|
53
|
+
|
54
|
+
# Check exact matches and subdomain matches
|
55
|
+
for allowed in self._allowed_sites:
|
56
|
+
allowed = allowed.lower()
|
57
|
+
if domain == allowed or domain.endswith('.' + allowed):
|
58
|
+
return True
|
59
|
+
|
60
|
+
return False
|
61
|
+
except Exception:
|
62
|
+
return False # Invalid URLs are blocked
|
63
|
+
|
64
|
+
def add_allowed_site(self, site: str) -> bool:
|
65
|
+
"""Add a site to the whitelist."""
|
66
|
+
# Clean up the site format
|
67
|
+
site = site.strip().lower()
|
68
|
+
if site.startswith('http://') or site.startswith('https://'):
|
69
|
+
parsed = urlparse(site)
|
70
|
+
site = parsed.netloc
|
71
|
+
|
72
|
+
if site and site not in self._allowed_sites:
|
73
|
+
self._allowed_sites.add(site)
|
74
|
+
self._save_whitelist()
|
75
|
+
return True
|
76
|
+
return False
|
77
|
+
|
78
|
+
def remove_allowed_site(self, site: str) -> bool:
|
79
|
+
"""Remove a site from the whitelist."""
|
80
|
+
site = site.strip().lower()
|
81
|
+
if site.startswith('http://') or site.startswith('https://'):
|
82
|
+
parsed = urlparse(site)
|
83
|
+
site = parsed.netloc
|
84
|
+
|
85
|
+
if site in self._allowed_sites:
|
86
|
+
self._allowed_sites.remove(site)
|
87
|
+
self._save_whitelist()
|
88
|
+
return True
|
89
|
+
return False
|
90
|
+
|
91
|
+
def get_allowed_sites(self) -> List[str]:
|
92
|
+
"""Get the list of allowed sites."""
|
93
|
+
return sorted(self._allowed_sites)
|
94
|
+
|
95
|
+
def set_allowed_sites(self, sites: List[str]):
|
96
|
+
"""Set the complete list of allowed sites."""
|
97
|
+
self._allowed_sites = set()
|
98
|
+
for site in sites:
|
99
|
+
site = site.strip().lower()
|
100
|
+
if site.startswith('http://') or site.startswith('https://'):
|
101
|
+
parsed = urlparse(site)
|
102
|
+
site = parsed.netloc
|
103
|
+
if site:
|
104
|
+
self._allowed_sites.add(site)
|
105
|
+
self._save_whitelist()
|
106
|
+
|
107
|
+
def clear_whitelist(self):
|
108
|
+
"""Clear all allowed sites."""
|
109
|
+
self._allowed_sites.clear()
|
110
|
+
self._save_whitelist()
|
111
|
+
|
112
|
+
|
113
|
+
# Global singleton
|
114
|
+
_url_whitelist_manager = None
|
115
|
+
|
116
|
+
def get_url_whitelist_manager() -> UrlWhitelistManager:
|
117
|
+
"""Get the global URL whitelist manager instance."""
|
118
|
+
global _url_whitelist_manager
|
119
|
+
if _url_whitelist_manager is None:
|
120
|
+
_url_whitelist_manager = UrlWhitelistManager()
|
121
|
+
return _url_whitelist_manager
|