hanzo-mcp 0.9.0__py3-none-any.whl → 0.9.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.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (135) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/analytics/posthog_analytics.py +14 -1
  3. hanzo_mcp/cli.py +108 -4
  4. hanzo_mcp/server.py +11 -0
  5. hanzo_mcp/tools/__init__.py +3 -16
  6. hanzo_mcp/tools/agent/__init__.py +5 -0
  7. hanzo_mcp/tools/agent/agent.py +5 -0
  8. hanzo_mcp/tools/agent/agent_tool.py +3 -17
  9. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +623 -0
  10. hanzo_mcp/tools/agent/clarification_tool.py +7 -1
  11. hanzo_mcp/tools/agent/claude_desktop_auth.py +16 -6
  12. hanzo_mcp/tools/agent/cli_agent_base.py +5 -0
  13. hanzo_mcp/tools/agent/cli_tools.py +26 -0
  14. hanzo_mcp/tools/agent/code_auth_tool.py +5 -0
  15. hanzo_mcp/tools/agent/critic_tool.py +7 -1
  16. hanzo_mcp/tools/agent/iching_tool.py +5 -0
  17. hanzo_mcp/tools/agent/network_tool.py +5 -0
  18. hanzo_mcp/tools/agent/review_tool.py +7 -1
  19. hanzo_mcp/tools/agent/swarm_alias.py +5 -0
  20. hanzo_mcp/tools/agent/swarm_tool.py +701 -0
  21. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +554 -0
  22. hanzo_mcp/tools/agent/unified_cli_tools.py +5 -0
  23. hanzo_mcp/tools/common/auto_timeout.py +254 -0
  24. hanzo_mcp/tools/common/base.py +4 -0
  25. hanzo_mcp/tools/common/batch_tool.py +5 -0
  26. hanzo_mcp/tools/common/config_tool.py +5 -0
  27. hanzo_mcp/tools/common/critic_tool.py +5 -0
  28. hanzo_mcp/tools/common/paginated_base.py +4 -0
  29. hanzo_mcp/tools/common/permissions.py +38 -12
  30. hanzo_mcp/tools/common/personality.py +673 -980
  31. hanzo_mcp/tools/common/stats.py +5 -0
  32. hanzo_mcp/tools/common/thinking_tool.py +5 -0
  33. hanzo_mcp/tools/common/timeout_parser.py +103 -0
  34. hanzo_mcp/tools/common/tool_disable.py +5 -0
  35. hanzo_mcp/tools/common/tool_enable.py +5 -0
  36. hanzo_mcp/tools/common/tool_list.py +5 -0
  37. hanzo_mcp/tools/config/config_tool.py +5 -0
  38. hanzo_mcp/tools/config/mode_tool.py +5 -0
  39. hanzo_mcp/tools/database/graph.py +5 -0
  40. hanzo_mcp/tools/database/graph_add.py +5 -0
  41. hanzo_mcp/tools/database/graph_query.py +5 -0
  42. hanzo_mcp/tools/database/graph_remove.py +5 -0
  43. hanzo_mcp/tools/database/graph_search.py +5 -0
  44. hanzo_mcp/tools/database/graph_stats.py +5 -0
  45. hanzo_mcp/tools/database/sql.py +5 -0
  46. hanzo_mcp/tools/database/sql_query.py +2 -0
  47. hanzo_mcp/tools/database/sql_search.py +5 -0
  48. hanzo_mcp/tools/database/sql_stats.py +5 -0
  49. hanzo_mcp/tools/editor/neovim_command.py +5 -0
  50. hanzo_mcp/tools/editor/neovim_edit.py +7 -2
  51. hanzo_mcp/tools/editor/neovim_session.py +5 -0
  52. hanzo_mcp/tools/filesystem/__init__.py +23 -26
  53. hanzo_mcp/tools/filesystem/ast_tool.py +3 -4
  54. hanzo_mcp/tools/filesystem/base.py +2 -18
  55. hanzo_mcp/tools/filesystem/batch_search.py +825 -0
  56. hanzo_mcp/tools/filesystem/content_replace.py +5 -3
  57. hanzo_mcp/tools/filesystem/diff.py +5 -0
  58. hanzo_mcp/tools/filesystem/directory_tree.py +34 -281
  59. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +345 -0
  60. hanzo_mcp/tools/filesystem/edit.py +6 -5
  61. hanzo_mcp/tools/filesystem/find.py +177 -311
  62. hanzo_mcp/tools/filesystem/find_files.py +370 -0
  63. hanzo_mcp/tools/filesystem/git_search.py +5 -3
  64. hanzo_mcp/tools/filesystem/grep.py +454 -0
  65. hanzo_mcp/tools/filesystem/multi_edit.py +6 -5
  66. hanzo_mcp/tools/filesystem/read.py +10 -9
  67. hanzo_mcp/tools/filesystem/rules_tool.py +6 -4
  68. hanzo_mcp/tools/filesystem/search_tool.py +728 -0
  69. hanzo_mcp/tools/filesystem/symbols_tool.py +510 -0
  70. hanzo_mcp/tools/filesystem/tree.py +273 -0
  71. hanzo_mcp/tools/filesystem/watch.py +6 -1
  72. hanzo_mcp/tools/filesystem/write.py +13 -7
  73. hanzo_mcp/tools/jupyter/jupyter.py +30 -2
  74. hanzo_mcp/tools/jupyter/notebook_edit.py +298 -0
  75. hanzo_mcp/tools/jupyter/notebook_read.py +148 -0
  76. hanzo_mcp/tools/llm/consensus_tool.py +8 -6
  77. hanzo_mcp/tools/llm/llm_manage.py +5 -0
  78. hanzo_mcp/tools/llm/llm_tool.py +2 -0
  79. hanzo_mcp/tools/llm/llm_unified.py +5 -0
  80. hanzo_mcp/tools/llm/provider_tools.py +5 -0
  81. hanzo_mcp/tools/lsp/lsp_tool.py +475 -622
  82. hanzo_mcp/tools/mcp/mcp_add.py +7 -2
  83. hanzo_mcp/tools/mcp/mcp_remove.py +15 -2
  84. hanzo_mcp/tools/mcp/mcp_stats.py +5 -0
  85. hanzo_mcp/tools/mcp/mcp_tool.py +5 -0
  86. hanzo_mcp/tools/memory/knowledge_tools.py +14 -0
  87. hanzo_mcp/tools/memory/memory_tools.py +17 -0
  88. hanzo_mcp/tools/search/find_tool.py +5 -3
  89. hanzo_mcp/tools/search/unified_search.py +3 -1
  90. hanzo_mcp/tools/shell/__init__.py +2 -14
  91. hanzo_mcp/tools/shell/base_process.py +4 -2
  92. hanzo_mcp/tools/shell/bash_tool.py +2 -0
  93. hanzo_mcp/tools/shell/command_executor.py +7 -7
  94. hanzo_mcp/tools/shell/logs.py +5 -0
  95. hanzo_mcp/tools/shell/npx.py +5 -0
  96. hanzo_mcp/tools/shell/npx_background.py +5 -0
  97. hanzo_mcp/tools/shell/npx_tool.py +5 -0
  98. hanzo_mcp/tools/shell/open.py +5 -0
  99. hanzo_mcp/tools/shell/pkill.py +5 -0
  100. hanzo_mcp/tools/shell/process_tool.py +5 -0
  101. hanzo_mcp/tools/shell/processes.py +5 -0
  102. hanzo_mcp/tools/shell/run_background.py +5 -0
  103. hanzo_mcp/tools/shell/run_command.py +2 -0
  104. hanzo_mcp/tools/shell/run_command_windows.py +5 -0
  105. hanzo_mcp/tools/shell/streaming_command.py +5 -0
  106. hanzo_mcp/tools/shell/uvx.py +5 -0
  107. hanzo_mcp/tools/shell/uvx_background.py +5 -0
  108. hanzo_mcp/tools/shell/uvx_tool.py +5 -0
  109. hanzo_mcp/tools/shell/zsh_tool.py +3 -0
  110. hanzo_mcp/tools/todo/todo.py +5 -0
  111. hanzo_mcp/tools/todo/todo_read.py +142 -0
  112. hanzo_mcp/tools/todo/todo_write.py +367 -0
  113. hanzo_mcp/tools/vector/__init__.py +42 -95
  114. hanzo_mcp/tools/vector/index_tool.py +5 -0
  115. hanzo_mcp/tools/vector/vector.py +5 -0
  116. hanzo_mcp/tools/vector/vector_index.py +5 -0
  117. hanzo_mcp/tools/vector/vector_search.py +5 -0
  118. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/METADATA +1 -1
  119. hanzo_mcp-0.9.2.dist-info/RECORD +195 -0
  120. hanzo_mcp/tools/common/path_utils.py +0 -34
  121. hanzo_mcp/tools/compiler/__init__.py +0 -8
  122. hanzo_mcp/tools/compiler/sandboxed_compiler.py +0 -681
  123. hanzo_mcp/tools/environment/__init__.py +0 -8
  124. hanzo_mcp/tools/environment/environment_detector.py +0 -594
  125. hanzo_mcp/tools/filesystem/search.py +0 -1160
  126. hanzo_mcp/tools/framework/__init__.py +0 -8
  127. hanzo_mcp/tools/framework/framework_modes.py +0 -714
  128. hanzo_mcp/tools/memory/conversation_memory.py +0 -636
  129. hanzo_mcp/tools/shell/run_tool.py +0 -56
  130. hanzo_mcp/tools/vector/node_tool.py +0 -538
  131. hanzo_mcp/tools/vector/unified_vector.py +0 -384
  132. hanzo_mcp-0.9.0.dist-info/RECORD +0 -191
  133. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/WHEEL +0 -0
  134. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/entry_points.txt +0 -0
  135. {hanzo_mcp-0.9.0.dist-info → hanzo_mcp-0.9.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,254 @@
1
+ """Universal auto-timeout and backgrounding for all MCP tools.
2
+
3
+ This module provides automatic timeout and backgrounding for any MCP tool operation
4
+ that takes longer than the configured threshold (default: 2 minutes).
5
+ """
6
+
7
+ import asyncio
8
+ import functools
9
+ import json
10
+ import time
11
+ import uuid
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Any, Callable, Optional, Tuple
15
+ from collections.abc import Awaitable
16
+
17
+ from mcp.server.fastmcp import Context as MCPContext
18
+ from .timeout_parser import parse_timeout, format_timeout
19
+
20
+
21
+ class MCPToolTimeoutManager:
22
+ """Manager for MCP tool timeouts and backgrounding."""
23
+
24
+ # Default timeout before auto-backgrounding (2 minutes)
25
+ DEFAULT_TIMEOUT = 120.0
26
+
27
+ # Environment variable to configure timeout
28
+ TIMEOUT_ENV_VAR = "HANZO_MCP_TOOL_TIMEOUT"
29
+
30
+ def __init__(self, process_manager: Optional[Any] = None):
31
+ """Initialize the timeout manager.
32
+
33
+ Args:
34
+ process_manager: Process manager for tracking background operations
35
+ """
36
+ if process_manager is None:
37
+ # Lazy import to avoid circular imports
38
+ try:
39
+ from hanzo_mcp.tools.shell.base_process import ProcessManager
40
+ self.process_manager = ProcessManager()
41
+ except ImportError:
42
+ # If ProcessManager is not available, disable backgrounding
43
+ self.process_manager = None
44
+ else:
45
+ self.process_manager = process_manager
46
+
47
+ # Get timeout from environment or use default
48
+ env_timeout = os.getenv(self.TIMEOUT_ENV_VAR)
49
+ if env_timeout:
50
+ try:
51
+ self.timeout = parse_timeout(env_timeout)
52
+ except ValueError:
53
+ self.timeout = self.DEFAULT_TIMEOUT
54
+ else:
55
+ self.timeout = self.DEFAULT_TIMEOUT
56
+
57
+ def _get_timeout_for_tool(self, tool_name: str) -> float:
58
+ """Get timeout setting for a specific tool.
59
+
60
+ Args:
61
+ tool_name: Name of the tool
62
+
63
+ Returns:
64
+ Timeout in seconds
65
+ """
66
+ # Check for tool-specific timeout
67
+ env_var = f"HANZO_MCP_{tool_name.upper()}_TIMEOUT"
68
+ tool_timeout = os.getenv(env_var)
69
+ if tool_timeout:
70
+ try:
71
+ return parse_timeout(tool_timeout)
72
+ except ValueError:
73
+ pass
74
+
75
+ return self.timeout
76
+
77
+ async def _background_tool_execution(
78
+ self,
79
+ tool_func: Callable,
80
+ tool_name: str,
81
+ ctx: MCPContext,
82
+ process_id: str,
83
+ log_file: Path,
84
+ **params: Any
85
+ ) -> None:
86
+ """Execute tool in background and log results.
87
+
88
+ Args:
89
+ tool_func: The tool function to execute
90
+ tool_name: Name of the tool
91
+ ctx: MCP context
92
+ process_id: Process identifier
93
+ log_file: Log file path
94
+ **params: Tool parameters
95
+ """
96
+ try:
97
+ # Log start
98
+ with open(log_file, "a") as f:
99
+ f.write(f"=== Background execution started for {tool_name} ===\\n")
100
+ f.write(f"Parameters: {json.dumps(params, indent=2, default=str)}\\n")
101
+ f.write(f"Started at: {time.strftime('%Y-%m-%d %H:%M:%S')}\\n\\n")
102
+
103
+ # Execute the tool
104
+ result = await tool_func(ctx, **params)
105
+
106
+ # Log completion
107
+ with open(log_file, "a") as f:
108
+ f.write(f"\\n\\n=== Tool execution completed ===\\n")
109
+ f.write(f"Completed at: {time.strftime('%Y-%m-%d %H:%M:%S')}\\n")
110
+ f.write(f"Result length: {len(str(result))} characters\\n")
111
+ f.write("\\n=== RESULT ===\\n")
112
+ f.write(str(result))
113
+ f.write("\\n=== END RESULT ===\\n")
114
+
115
+ # Mark as completed
116
+ self.process_manager.mark_completed(process_id, 0)
117
+
118
+ except Exception as e:
119
+ # Log error
120
+ with open(log_file, "a") as f:
121
+ f.write(f"\\n\\n=== Tool execution failed ===\\n")
122
+ f.write(f"Failed at: {time.strftime('%Y-%m-%d %H:%M:%S')}\\n")
123
+ f.write(f"Error: {str(e)}\\n")
124
+ f.write(f"Error type: {type(e).__name__}\\n")
125
+
126
+ self.process_manager.mark_completed(process_id, 1)
127
+
128
+
129
+ def with_auto_timeout(tool_name: str, timeout_manager: Optional[MCPToolTimeoutManager] = None):
130
+ """Decorator to add automatic timeout and backgrounding to MCP tools.
131
+
132
+ Args:
133
+ tool_name: Name of the tool (for logging and process tracking)
134
+ timeout_manager: Optional timeout manager instance
135
+
136
+ Returns:
137
+ Decorator function
138
+ """
139
+ if timeout_manager is None:
140
+ timeout_manager = MCPToolTimeoutManager()
141
+
142
+ def decorator(func: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
143
+ @functools.wraps(func)
144
+ async def wrapper(*args: Any, **params: Any) -> Any:
145
+ # Handle both method calls (with self) and function calls
146
+ # For methods: args = (self, ctx), For functions: args = (ctx,)
147
+ if len(args) >= 2:
148
+ # Method call: self, ctx, **params
149
+ self_or_ctx = args[0]
150
+ ctx = args[1]
151
+ call_func = lambda: func(self_or_ctx, ctx, **params)
152
+ elif len(args) == 1:
153
+ # Function call: ctx, **params
154
+ ctx = args[0]
155
+ call_func = lambda: func(ctx, **params)
156
+ else:
157
+ raise TypeError(f"Expected at least 1 argument (ctx), got {len(args)}")
158
+
159
+ # Fast path for tests - skip timeout logic
160
+ if os.getenv("HANZO_MCP_FAST_TESTS") == "1":
161
+ return await call_func()
162
+
163
+ # Get tool-specific timeout
164
+ tool_timeout = timeout_manager._get_timeout_for_tool(tool_name)
165
+
166
+ # Create task for the tool execution
167
+ tool_task = asyncio.create_task(call_func())
168
+
169
+ try:
170
+ # Wait for completion with timeout
171
+ result = await asyncio.wait_for(tool_task, timeout=tool_timeout)
172
+ return result
173
+
174
+ except asyncio.TimeoutError:
175
+ # Tool timed out - background it
176
+ process_id = f"{tool_name}_{uuid.uuid4().hex[:8]}"
177
+ log_file = timeout_manager.process_manager.create_log_file(process_id)
178
+
179
+ # Start background execution (need to reconstruct the call)
180
+ async def background_call():
181
+ if len(args) >= 2:
182
+ return await func(args[0], ctx, **params)
183
+ else:
184
+ return await func(ctx, **params)
185
+
186
+ asyncio.create_task(
187
+ timeout_manager._background_tool_execution(
188
+ background_call, tool_name, ctx, process_id, log_file, **params
189
+ )
190
+ )
191
+
192
+ # Return backgrounding message
193
+ timeout_formatted = format_timeout(tool_timeout)
194
+ return (
195
+ f"Operation automatically backgrounded after {timeout_formatted}\\n"
196
+ f"Process ID: {process_id}\\n"
197
+ f"Log file: {log_file}\\n\\n"
198
+ f"Use 'process --action logs --id {process_id}' to view results\\n"
199
+ f"Use 'process --action kill --id {process_id}' to cancel\\n\\n"
200
+ f"The {tool_name} operation is continuing in the background..."
201
+ )
202
+
203
+ return wrapper
204
+ return decorator
205
+
206
+
207
+ # Global timeout manager instance
208
+ _global_timeout_manager = None
209
+
210
+
211
+ def get_global_timeout_manager() -> MCPToolTimeoutManager:
212
+ """Get the global timeout manager instance.
213
+
214
+ Returns:
215
+ Global timeout manager
216
+ """
217
+ global _global_timeout_manager
218
+ if _global_timeout_manager is None:
219
+ _global_timeout_manager = MCPToolTimeoutManager()
220
+ return _global_timeout_manager
221
+
222
+
223
+ def set_global_timeout(timeout_seconds: float) -> None:
224
+ """Set the global timeout for all MCP tools.
225
+
226
+ Args:
227
+ timeout_seconds: Timeout in seconds
228
+ """
229
+ manager = get_global_timeout_manager()
230
+ manager.timeout = timeout_seconds
231
+
232
+
233
+ def set_tool_timeout(tool_name: str, timeout_seconds: float) -> None:
234
+ """Set timeout for a specific tool via environment variable.
235
+
236
+ Args:
237
+ tool_name: Name of the tool
238
+ timeout_seconds: Timeout in seconds
239
+ """
240
+ env_var = f"HANZO_MCP_{tool_name.upper()}_TIMEOUT"
241
+ os.environ[env_var] = str(timeout_seconds)
242
+
243
+
244
+ # Convenience decorator using global manager
245
+ def auto_timeout(tool_name: str):
246
+ """Convenience decorator using the global timeout manager.
247
+
248
+ Args:
249
+ tool_name: Name of the tool
250
+
251
+ Returns:
252
+ Decorator function
253
+ """
254
+ return with_auto_timeout(tool_name, get_global_timeout_manager())
@@ -11,6 +11,8 @@ from typing import Any, Callable, final
11
11
  from collections.abc import Awaitable
12
12
 
13
13
  from mcp.server import FastMCP
14
+
15
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
14
16
  from mcp.server.fastmcp import Context as MCPContext
15
17
 
16
18
  from hanzo_mcp.tools.common.validation import (
@@ -88,6 +90,8 @@ class BaseTool(ABC):
88
90
  pass
89
91
 
90
92
  @abstractmethod
93
+ @auto_timeout("base")
94
+
91
95
  async def call(self, ctx: MCPContext, **params: Any) -> Any:
92
96
  """Execute the tool with the given parameters.
93
97
 
@@ -9,6 +9,8 @@ from typing import Any, Unpack, Annotated, TypedDict, final, override
9
9
 
10
10
  from pydantic import Field
11
11
  from mcp.server import FastMCP
12
+
13
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
12
14
  from mcp.server.fastmcp import Context as MCPContext
13
15
 
14
16
  from hanzo_mcp.tools.common.base import BaseTool
@@ -154,6 +156,9 @@ Not available: think,write,edit,multi_edit,notebook_edit
154
156
  self.tools = tools
155
157
 
156
158
  @override
159
+ @auto_timeout("batch")
160
+
161
+
157
162
  async def call(
158
163
  self,
159
164
  ctx: MCPContext,
@@ -5,6 +5,8 @@ from typing import Any, Dict, Unpack, Optional, TypedDict, final
5
5
 
6
6
  from mcp.server.fastmcp import Context as MCPContext
7
7
 
8
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
9
+
8
10
  from hanzo_mcp.config.settings import (
9
11
  ProjectConfig,
10
12
  MCPServerConfig,
@@ -62,6 +64,9 @@ Perfect for AI-driven configuration where users can say things like:
62
64
 
63
65
  Automatically detects projects based on LLM.md files and manages .hanzo/ directories."""
64
66
 
67
+ @auto_timeout("config")
68
+
69
+
65
70
  async def call(
66
71
  self,
67
72
  ctx: MCPContext,
@@ -7,6 +7,8 @@ from typing import Unpack, Annotated, TypedDict, final, override
7
7
 
8
8
  from pydantic import Field
9
9
  from mcp.server import FastMCP
10
+
11
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
10
12
  from mcp.server.fastmcp import Context as MCPContext
11
13
 
12
14
  from hanzo_mcp.tools.common.base import BaseTool
@@ -131,6 +133,9 @@ Recommendations:
131
133
  pass
132
134
 
133
135
  @override
136
+ @auto_timeout("critic")
137
+
138
+
134
139
  async def call(
135
140
  self,
136
141
  ctx: MCPContext,
@@ -9,6 +9,8 @@ from typing import Any, Dict, Union
9
9
 
10
10
  from mcp.server.fastmcp import Context as MCPContext
11
11
 
12
+ from hanzo_mcp.tools.common.auto_timeout import auto_timeout
13
+
12
14
  from hanzo_mcp.tools.common.base import BaseTool, handle_connection_errors
13
15
  from hanzo_mcp.tools.common.pagination import CursorManager
14
16
  from hanzo_mcp.tools.common.paginated_response import paginate_if_needed
@@ -44,6 +46,8 @@ class PaginatedBaseTool(BaseTool):
44
46
  pass
45
47
 
46
48
  @handle_connection_errors
49
+ @auto_timeout("paginated_base")
50
+
47
51
  async def call(self, ctx: MCPContext, **params: Any) -> Union[str, Dict[str, Any]]:
48
52
  """Execute the tool with automatic pagination support.
49
53
 
@@ -8,9 +8,6 @@ from typing import Any, TypeVar, final
8
8
  from pathlib import Path
9
9
  from collections.abc import Callable, Awaitable
10
10
 
11
- # Import centralized path resolution
12
- from hanzo_mcp.tools.common.path_utils import resolve_path
13
-
14
11
  # Define type variables for better type annotations
15
12
  T = TypeVar("T")
16
13
  P = TypeVar("P")
@@ -84,8 +81,7 @@ class PermissionManager:
84
81
  Args:
85
82
  path: The path to allow
86
83
  """
87
- # Use centralized path resolution
88
- resolved_path = Path(resolve_path(path))
84
+ resolved_path: Path = Path(path).resolve()
89
85
  self.allowed_paths.add(resolved_path)
90
86
 
91
87
  def remove_allowed_path(self, path: str) -> None:
@@ -94,8 +90,7 @@ class PermissionManager:
94
90
  Args:
95
91
  path: The path to remove
96
92
  """
97
- # Use centralized path resolution
98
- resolved_path = Path(resolve_path(path))
93
+ resolved_path: Path = Path(path).resolve()
99
94
  if resolved_path in self.allowed_paths:
100
95
  self.allowed_paths.remove(resolved_path)
101
96
 
@@ -105,8 +100,7 @@ class PermissionManager:
105
100
  Args:
106
101
  path: The path to exclude
107
102
  """
108
- # Use centralized path resolution
109
- resolved_path = Path(resolve_path(path))
103
+ resolved_path: Path = Path(path).resolve()
110
104
  self.excluded_paths.add(resolved_path)
111
105
 
112
106
  def add_exclusion_pattern(self, pattern: str) -> None:
@@ -118,7 +112,7 @@ class PermissionManager:
118
112
  self.excluded_patterns.append(pattern)
119
113
 
120
114
  def is_path_allowed(self, path: str) -> bool:
121
- """Check if a path is allowed.
115
+ """Check if a path is allowed with security validation.
122
116
 
123
117
  Args:
124
118
  path: The path to check
@@ -126,8 +120,24 @@ class PermissionManager:
126
120
  Returns:
127
121
  True if the path is allowed, False otherwise
128
122
  """
129
- # Use centralized path resolution
130
- resolved_path = Path(resolve_path(path))
123
+ # Security check: Reject paths with traversal attempts
124
+ if ".." in str(path) or "~" in str(path):
125
+ return False
126
+
127
+ try:
128
+ # Resolve the path (follows symlinks and makes absolute)
129
+ resolved_path: Path = Path(path).resolve(strict=False)
130
+
131
+ # Security check: Ensure resolved path doesn't escape allowed directories
132
+ # by checking if it's actually under an allowed path after resolution
133
+ original_path = Path(path)
134
+ if original_path.is_absolute() and str(resolved_path) != str(original_path.resolve(strict=False)):
135
+ # Path resolution changed the path significantly, might be symlink attack
136
+ # Additional check: is the resolved path still under allowed paths?
137
+ pass # Continue to normal checks
138
+ except (OSError, RuntimeError) as e:
139
+ # Path resolution failed, deny access
140
+ return False
131
141
 
132
142
  # Check exclusions first
133
143
  if self._is_path_excluded(resolved_path):
@@ -136,12 +146,28 @@ class PermissionManager:
136
146
  # Check if the path is within any allowed path
137
147
  for allowed_path in self.allowed_paths:
138
148
  try:
149
+ # This will raise ValueError if resolved_path is not under allowed_path
139
150
  resolved_path.relative_to(allowed_path)
151
+ # Additional check: ensure no symlinks are escaping the allowed directory
152
+ if resolved_path.exists() and resolved_path.is_symlink():
153
+ link_target = Path(os.readlink(resolved_path))
154
+ if link_target.is_absolute():
155
+ # Absolute symlink - check if it points within allowed paths
156
+ if not any(self._is_subpath(link_target, ap) for ap in self.allowed_paths):
157
+ return False
140
158
  return True
141
159
  except ValueError:
142
160
  continue
143
161
 
144
162
  return False
163
+
164
+ def _is_subpath(self, child: Path, parent: Path) -> bool:
165
+ """Check if child is a subpath of parent."""
166
+ try:
167
+ child.resolve().relative_to(parent.resolve())
168
+ return True
169
+ except ValueError:
170
+ return False
145
171
 
146
172
  def _is_path_excluded(self, path: Path) -> bool:
147
173
  """Check if a path is excluded.