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.
Files changed (67) hide show
  1. janito/README.md +0 -0
  2. janito/agent/setup_agent.py +14 -0
  3. janito/agent/templates/profiles/system_prompt_template_Developer_with_Python_Tools.txt.j2 +59 -11
  4. janito/agent/templates/profiles/system_prompt_template_developer.txt.j2 +53 -7
  5. janito/agent/templates/profiles/system_prompt_template_market_analyst.txt.j2 +108 -8
  6. janito/agent/templates/profiles/system_prompt_template_model_conversation_without_tools_or_context.txt.j2 +53 -1
  7. janito/cli/chat_mode/session.py +8 -1
  8. janito/cli/chat_mode/shell/commands/__init__.py +2 -0
  9. janito/cli/chat_mode/shell/commands/security/__init__.py +1 -0
  10. janito/cli/chat_mode/shell/commands/security/allowed_sites.py +94 -0
  11. janito/cli/chat_mode/shell/commands/security_command.py +51 -0
  12. janito/cli/chat_mode/shell/commands.bak.zip +0 -0
  13. janito/cli/chat_mode/shell/session.bak.zip +0 -0
  14. janito/cli/cli_commands/list_plugins.py +45 -0
  15. janito/cli/cli_commands/show_system_prompt.py +13 -40
  16. janito/cli/core/getters.py +4 -0
  17. janito/cli/core/runner.py +7 -2
  18. janito/cli/core/setters.py +10 -1
  19. janito/cli/main_cli.py +25 -3
  20. janito/cli/single_shot_mode/handler.py +3 -1
  21. janito/config_manager.py +10 -0
  22. janito/docs/GETTING_STARTED.md +0 -0
  23. janito/drivers/dashscope.bak.zip +0 -0
  24. janito/drivers/openai/README.md +0 -0
  25. janito/drivers/openai_responses.bak.zip +0 -0
  26. janito/llm/README.md +0 -0
  27. janito/mkdocs.yml +0 -0
  28. janito/plugins/__init__.py +17 -0
  29. janito/plugins/base.py +93 -0
  30. janito/plugins/discovery.py +160 -0
  31. janito/plugins/manager.py +185 -0
  32. janito/providers/dashscope.bak.zip +0 -0
  33. janito/providers/ibm/README.md +0 -0
  34. janito/shell.bak.zip +0 -0
  35. janito/tools/DOCSTRING_STANDARD.txt +0 -0
  36. janito/tools/README.md +0 -0
  37. janito/tools/adapters/local/__init__.py +2 -0
  38. janito/tools/adapters/local/adapter.py +55 -0
  39. janito/tools/adapters/local/ask_user.py +2 -0
  40. janito/tools/adapters/local/fetch_url.py +89 -4
  41. janito/tools/adapters/local/find_files.py +2 -0
  42. janito/tools/adapters/local/get_file_outline/core.py +2 -0
  43. janito/tools/adapters/local/get_file_outline/search_outline.py +2 -0
  44. janito/tools/adapters/local/open_html_in_browser.py +2 -0
  45. janito/tools/adapters/local/open_url.py +2 -0
  46. janito/tools/adapters/local/python_code_run.py +15 -10
  47. janito/tools/adapters/local/python_command_run.py +14 -9
  48. janito/tools/adapters/local/python_file_run.py +15 -10
  49. janito/tools/adapters/local/read_chart.py +252 -0
  50. janito/tools/adapters/local/read_files.py +2 -0
  51. janito/tools/adapters/local/replace_text_in_file.py +1 -1
  52. janito/tools/adapters/local/run_bash_command.py +18 -12
  53. janito/tools/adapters/local/run_powershell_command.py +15 -9
  54. janito/tools/adapters/local/search_text/core.py +2 -0
  55. janito/tools/adapters/local/validate_file_syntax/core.py +6 -0
  56. janito/tools/adapters/local/validate_file_syntax/jinja2_validator.py +47 -0
  57. janito/tools/adapters/local/view_file.py +2 -0
  58. janito/tools/loop_protection.py +115 -0
  59. janito/tools/loop_protection_decorator.py +110 -0
  60. janito/tools/outline_file.bak.zip +0 -0
  61. janito/tools/url_whitelist.py +121 -0
  62. {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/METADATA +411 -411
  63. {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/RECORD +52 -39
  64. {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/entry_points.txt +0 -0
  65. {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/licenses/LICENSE +0 -0
  66. {janito-2.22.0.dist-info → janito-2.24.0.dist-info}/top_level.txt +0 -0
  67. {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