hanzo-mcp 0.1.25__py3-none-any.whl → 0.1.30__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 (48) hide show
  1. hanzo_mcp/__init__.py +2 -2
  2. hanzo_mcp/cli.py +80 -9
  3. hanzo_mcp/server.py +41 -10
  4. hanzo_mcp/tools/__init__.py +51 -32
  5. hanzo_mcp/tools/agent/__init__.py +59 -0
  6. hanzo_mcp/tools/agent/agent_tool.py +474 -0
  7. hanzo_mcp/tools/agent/prompt.py +137 -0
  8. hanzo_mcp/tools/agent/tool_adapter.py +75 -0
  9. hanzo_mcp/tools/common/__init__.py +17 -0
  10. hanzo_mcp/tools/common/base.py +216 -0
  11. hanzo_mcp/tools/common/context.py +7 -3
  12. hanzo_mcp/tools/common/permissions.py +63 -119
  13. hanzo_mcp/tools/common/session.py +91 -0
  14. hanzo_mcp/tools/common/thinking_tool.py +123 -0
  15. hanzo_mcp/tools/filesystem/__init__.py +85 -5
  16. hanzo_mcp/tools/filesystem/base.py +113 -0
  17. hanzo_mcp/tools/filesystem/content_replace.py +287 -0
  18. hanzo_mcp/tools/filesystem/directory_tree.py +286 -0
  19. hanzo_mcp/tools/filesystem/edit_file.py +287 -0
  20. hanzo_mcp/tools/filesystem/get_file_info.py +170 -0
  21. hanzo_mcp/tools/filesystem/read_files.py +198 -0
  22. hanzo_mcp/tools/filesystem/search_content.py +275 -0
  23. hanzo_mcp/tools/filesystem/write_file.py +162 -0
  24. hanzo_mcp/tools/jupyter/__init__.py +67 -4
  25. hanzo_mcp/tools/jupyter/base.py +284 -0
  26. hanzo_mcp/tools/jupyter/edit_notebook.py +295 -0
  27. hanzo_mcp/tools/jupyter/notebook_operations.py +72 -112
  28. hanzo_mcp/tools/jupyter/read_notebook.py +165 -0
  29. hanzo_mcp/tools/project/__init__.py +64 -1
  30. hanzo_mcp/tools/project/analysis.py +9 -6
  31. hanzo_mcp/tools/project/base.py +66 -0
  32. hanzo_mcp/tools/project/project_analyze.py +173 -0
  33. hanzo_mcp/tools/shell/__init__.py +58 -1
  34. hanzo_mcp/tools/shell/base.py +148 -0
  35. hanzo_mcp/tools/shell/command_executor.py +203 -322
  36. hanzo_mcp/tools/shell/run_command.py +204 -0
  37. hanzo_mcp/tools/shell/run_script.py +215 -0
  38. hanzo_mcp/tools/shell/script_tool.py +244 -0
  39. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/METADATA +72 -77
  40. hanzo_mcp-0.1.30.dist-info/RECORD +45 -0
  41. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/licenses/LICENSE +2 -2
  42. hanzo_mcp/tools/common/thinking.py +0 -65
  43. hanzo_mcp/tools/filesystem/file_operations.py +0 -1050
  44. hanzo_mcp-0.1.25.dist-info/RECORD +0 -24
  45. hanzo_mcp-0.1.25.dist-info/zip-safe +0 -1
  46. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/WHEEL +0 -0
  47. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/entry_points.txt +0 -0
  48. {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/top_level.txt +0 -0
@@ -1 +1,18 @@
1
1
  """Common utilities for Hanzo MCP tools."""
2
+
3
+ from mcp.server.fastmcp import FastMCP
4
+
5
+ from hanzo_mcp.tools.common.base import ToolRegistry
6
+ from hanzo_mcp.tools.common.thinking_tool import ThinkingTool
7
+
8
+
9
+ def register_thinking_tool(
10
+ mcp_server: FastMCP,
11
+ ) -> None:
12
+ """Register all thinking tools with the MCP server.
13
+
14
+ Args:
15
+ mcp_server: The FastMCP server instance
16
+ """
17
+ thinking_tool = ThinkingTool()
18
+ ToolRegistry.register_tool(mcp_server, thinking_tool)
@@ -0,0 +1,216 @@
1
+ """Base classes for Hanzo MCP tools.
2
+
3
+ This module provides abstract base classes that define interfaces and common functionality
4
+ for all tools used in Hanzo MCP. These abstractions help ensure consistent tool
5
+ behavior and provide a foundation for tool registration and management.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Any, final
10
+
11
+ from mcp.server.fastmcp import Context as MCPContext
12
+ from mcp.server.fastmcp import FastMCP
13
+
14
+ from hanzo_mcp.tools.common.context import DocumentContext
15
+ from hanzo_mcp.tools.common.permissions import PermissionManager
16
+ from hanzo_mcp.tools.common.validation import ValidationResult, validate_path_parameter
17
+
18
+
19
+ class BaseTool(ABC):
20
+ """Abstract base class for all Hanzo MCP tools.
21
+
22
+ This class defines the core interface that all tools must implement, ensuring
23
+ consistency in how tools are registered, documented, and called.
24
+ """
25
+
26
+ @property
27
+ @abstractmethod
28
+ def name(self) -> str:
29
+ """Get the tool name.
30
+
31
+ Returns:
32
+ The tool name as it will appear in the MCP server
33
+ """
34
+ pass
35
+
36
+ @property
37
+ @abstractmethod
38
+ def description(self) -> str:
39
+ """Get the tool description.
40
+
41
+ Returns:
42
+ Detailed description of the tool's purpose and usage
43
+ """
44
+ pass
45
+
46
+ @property
47
+ def mcp_description(self) -> str:
48
+ """Get the complete tool description for MCP.
49
+
50
+ This method combines the tool description with parameter descriptions.
51
+
52
+ Returns:
53
+ Complete tool description including parameter details
54
+ """
55
+ # Start with the base description
56
+ desc = self.description.strip()
57
+
58
+ # Add parameter descriptions section if there are parameters
59
+ if self.parameters and "properties" in self.parameters:
60
+ # Add Args section header
61
+ desc += "\n\nArgs:"
62
+
63
+ # Get the properties dictionary
64
+ properties = self.parameters["properties"]
65
+
66
+ # Add each parameter description
67
+ for param_name, param_info in properties.items():
68
+ # Get the title if available, otherwise use the parameter name and capitalize it
69
+ if "title" in param_info:
70
+ title = param_info["title"]
71
+ else:
72
+ # Convert snake_case to Title Case
73
+ title = " ".join(word.capitalize() for word in param_name.split("_"))
74
+
75
+ # Check if the parameter is required
76
+ required = param_name in self.required
77
+ required_text = "" if required else " (optional)"
78
+
79
+ # Add the parameter description line
80
+ desc += f"\n {param_name}: {title}{required_text}"
81
+
82
+ # Add Returns section
83
+ desc += "\n\nReturns:\n "
84
+
85
+ # Add a generic return description based on the tool's purpose
86
+ # This could be enhanced with more specific return descriptions
87
+ if "read" in self.name or "get" in self.name or "search" in self.name:
88
+ desc += f"{self.name.replace('_', ' ').capitalize()} results"
89
+ elif "write" in self.name or "edit" in self.name or "create" in self.name:
90
+ desc += "Result of the operation"
91
+ else:
92
+ desc += "Tool execution results"
93
+
94
+ return desc
95
+
96
+ @property
97
+ @abstractmethod
98
+ def parameters(self) -> dict[str, Any]:
99
+ """Get the parameter specifications for the tool.
100
+
101
+ Returns:
102
+ Dictionary containing parameter specifications in JSON Schema format
103
+ """
104
+ pass
105
+
106
+ @property
107
+ @abstractmethod
108
+ def required(self) -> list[str]:
109
+ """Get the list of required parameter names.
110
+
111
+ Returns:
112
+ List of parameter names that are required for the tool
113
+ """
114
+ pass
115
+
116
+ @abstractmethod
117
+ async def call(self, ctx: MCPContext, **params: Any) -> str:
118
+ """Execute the tool with the given parameters.
119
+
120
+ Args:
121
+ ctx: MCP context for the tool call
122
+ **params: Tool parameters provided by the caller
123
+
124
+ Returns:
125
+ Tool execution result as a string
126
+ """
127
+ pass
128
+
129
+ @abstractmethod
130
+ def register(self, mcp_server: FastMCP) -> None:
131
+ """Register this tool with the MCP server.
132
+
133
+ This method must be implemented by each tool class to create a wrapper function
134
+ with explicitly defined parameters that calls this tool's call method.
135
+ The wrapper function is then registered with the MCP server.
136
+
137
+ Args:
138
+ mcp_server: The FastMCP server instance
139
+ """
140
+ pass
141
+
142
+
143
+ class FileSystemTool(BaseTool,ABC):
144
+ """Base class for filesystem-related tools.
145
+
146
+ Provides common functionality for working with files and directories,
147
+ including permission checking and path validation.
148
+ """
149
+
150
+ def __init__(
151
+ self,
152
+ document_context: DocumentContext,
153
+ permission_manager: PermissionManager
154
+ ) -> None:
155
+ """Initialize filesystem tool.
156
+
157
+ Args:
158
+ document_context: Document context for tracking file contents
159
+ permission_manager: Permission manager for access control
160
+ """
161
+ self.document_context:DocumentContext = document_context
162
+ self.permission_manager:PermissionManager = permission_manager
163
+
164
+ def validate_path(self, path: str, param_name: str = "path") -> ValidationResult:
165
+ """Validate a path parameter.
166
+
167
+ Args:
168
+ path: Path to validate
169
+ param_name: Name of the parameter (for error messages)
170
+
171
+ Returns:
172
+ Validation result containing validation status and error message if any
173
+ """
174
+ return validate_path_parameter(path, param_name)
175
+
176
+ def is_path_allowed(self, path: str) -> bool:
177
+ """Check if a path is allowed according to permission settings.
178
+
179
+ Args:
180
+ path: Path to check
181
+
182
+ Returns:
183
+ True if the path is allowed, False otherwise
184
+ """
185
+ return self.permission_manager.is_path_allowed(path)
186
+
187
+
188
+ @final
189
+ class ToolRegistry:
190
+ """Registry for Hanzo MCP tools.
191
+
192
+ Provides functionality for registering tool implementations with an MCP server,
193
+ handling the conversion between tool classes and MCP tool functions.
194
+ """
195
+
196
+ @staticmethod
197
+ def register_tool(mcp_server: FastMCP, tool: BaseTool) -> None:
198
+ """Register a tool with the MCP server.
199
+
200
+ Args:
201
+ mcp_server: The FastMCP server instance
202
+ tool: The tool to register
203
+ """
204
+ # Use the tool's register method which handles all the details
205
+ tool.register(mcp_server)
206
+
207
+ @staticmethod
208
+ def register_tools(mcp_server: FastMCP, tools: list[BaseTool]) -> None:
209
+ """Register multiple tools with the MCP server.
210
+
211
+ Args:
212
+ mcp_server: The FastMCP server instance
213
+ tools: List of tools to register
214
+ """
215
+ for tool in tools:
216
+ ToolRegistry.register_tool(mcp_server, tool)
@@ -1,7 +1,7 @@
1
1
  """Enhanced Context for Hanzo MCP tools.
2
2
 
3
3
  This module provides an enhanced Context class that wraps the MCP Context
4
- and adds additional functionality specific to Claude Code tools.
4
+ and adds additional functionality specific to Hanzo tools.
5
5
  """
6
6
 
7
7
  import json
@@ -179,7 +179,9 @@ class DocumentContext:
179
179
  Args:
180
180
  path: The path to allow
181
181
  """
182
- resolved_path: Path = Path(path).resolve()
182
+ # Expand user path (e.g., ~/ or $HOME)
183
+ expanded_path = os.path.expanduser(path)
184
+ resolved_path: Path = Path(expanded_path).resolve()
183
185
  self.allowed_paths.add(resolved_path)
184
186
 
185
187
  def is_path_allowed(self, path: str) -> bool:
@@ -191,7 +193,9 @@ class DocumentContext:
191
193
  Returns:
192
194
  True if the path is allowed, False otherwise
193
195
  """
194
- resolved_path: Path = Path(path).resolve()
196
+ # Expand user path (e.g., ~/ or $HOME)
197
+ expanded_path = os.path.expanduser(path)
198
+ resolved_path: Path = Path(expanded_path).resolve()
195
199
 
196
200
  # Check if the path is within any allowed path
197
201
  for allowed_path in self.allowed_paths:
@@ -11,65 +11,29 @@ T = TypeVar("T")
11
11
  P = TypeVar("P")
12
12
 
13
13
 
14
- def normalize_path(path: str) -> Path:
15
- """Normalize a path with proper user directory expansion.
16
-
17
- This utility function handles path normalization with proper handling of
18
- tilde (~) for home directory expansion and ensures consistent path handling
19
- across the application.
20
-
21
- Args:
22
- path: The path to normalize (can include ~ for home directory)
23
-
24
- Returns:
25
- A normalized Path object with user directories expanded and resolved to
26
- its absolute canonical form.
27
- """
28
- # Expand the user directory, handling the tilde (~) if present.
29
- expanded_path = os.path.expanduser(path)
30
- # Resolve the expanded path to its absolute form.
31
- resolved_path = Path(expanded_path).resolve()
32
- return resolved_path
33
-
34
-
35
14
  @final
36
15
  class PermissionManager:
37
- """Manages permissions for file and command operations.
38
-
39
- This class is responsible for tracking allowed file system paths as well as
40
- paths and patterns that should be excluded from permitted operations.
41
- """
16
+ """Manages permissions for file and command operations."""
42
17
 
43
18
  def __init__(self) -> None:
44
- """Initialize the permission manager with default allowed and excluded paths.
45
-
46
- Allowed paths are those where operations (read, write, execute, etc.) are permitted,
47
- while excluded paths and patterns represent paths and file patterns that are sensitive
48
- and should be disallowed.
49
- """
50
- # Allowed paths: operations are permitted on these paths.
19
+ """Initialize the permission manager."""
20
+ # Allowed paths
51
21
  self.allowed_paths: set[Path] = set(
52
22
  [Path("/tmp").resolve(), Path("/var").resolve()]
53
23
  )
54
24
 
55
- # Excluded paths: specific paths that are explicitly disallowed.
25
+ # Excluded paths
56
26
  self.excluded_paths: set[Path] = set()
57
-
58
- # Excluded patterns: patterns for sensitive directories and file names.
59
27
  self.excluded_patterns: list[str] = []
60
28
 
61
- # Add default exclusions for sensitive files and directories.
29
+ # Default excluded patterns
62
30
  self._add_default_exclusions()
63
31
 
64
32
  def _add_default_exclusions(self) -> None:
65
- """Add default exclusions for sensitive files and directories.
66
-
67
- This method populates the excluded_patterns list with common sensitive
68
- directories (e.g., .ssh, .gnupg) and file patterns (e.g., *.key, *.log)
69
- that should be excluded from allowed operations.
70
- """
71
- # Sensitive directories (Note: .git is allowed by default)
33
+ """Add default exclusions for sensitive files and directories."""
34
+ # Sensitive directories
72
35
  sensitive_dirs: list[str] = [
36
+ # ".git" is now allowed by default
73
37
  ".ssh",
74
38
  ".gnupg",
75
39
  ".config",
@@ -100,64 +64,63 @@ class PermissionManager:
100
64
  self.excluded_patterns.extend(sensitive_patterns)
101
65
 
102
66
  def add_allowed_path(self, path: str) -> None:
103
- """Add a new path to the allowed paths.
67
+ """Add a path to the allowed paths.
104
68
 
105
69
  Args:
106
- path: The file system path to add to the allowed list.
70
+ path: The path to allow
107
71
  """
108
- resolved_path: Path = normalize_path(path)
72
+ # Expand user path (e.g., ~/ or $HOME)
73
+ expanded_path = os.path.expanduser(path)
74
+ resolved_path: Path = Path(expanded_path).resolve()
109
75
  self.allowed_paths.add(resolved_path)
110
76
 
111
77
  def remove_allowed_path(self, path: str) -> None:
112
78
  """Remove a path from the allowed paths.
113
79
 
114
80
  Args:
115
- path: The file system path to remove from the allowed list.
81
+ path: The path to remove
116
82
  """
117
- resolved_path: Path = normalize_path(path)
83
+ resolved_path: Path = Path(path).resolve()
118
84
  if resolved_path in self.allowed_paths:
119
85
  self.allowed_paths.remove(resolved_path)
120
86
 
121
87
  def exclude_path(self, path: str) -> None:
122
- """Add a path to the exclusion list.
88
+ """Exclude a path from allowed operations.
123
89
 
124
90
  Args:
125
- path: The file system path to explicitly exclude from operations.
91
+ path: The path to exclude
126
92
  """
127
- resolved_path: Path = normalize_path(path)
93
+ resolved_path: Path = Path(path).resolve()
128
94
  self.excluded_paths.add(resolved_path)
129
95
 
130
96
  def add_exclusion_pattern(self, pattern: str) -> None:
131
- """Add a new exclusion pattern.
97
+ """Add an exclusion pattern.
132
98
 
133
99
  Args:
134
- pattern: A string pattern that matches file or directory names to exclude.
100
+ pattern: The pattern to exclude
135
101
  """
136
102
  self.excluded_patterns.append(pattern)
137
103
 
138
104
  def is_path_allowed(self, path: str) -> bool:
139
- """Determine if a given path is allowed for operations.
140
-
141
- The method normalizes the input path and then checks it against the list
142
- of excluded paths and patterns. If the path is not excluded and is a
143
- subpath of one of the allowed base paths, the method returns True.
105
+ """Check if a path is allowed.
144
106
 
145
107
  Args:
146
- path: The file system path to check.
108
+ path: The path to check
147
109
 
148
110
  Returns:
149
- True if the path is allowed for operations, False otherwise.
111
+ True if the path is allowed, False otherwise
150
112
  """
151
- resolved_path: Path = normalize_path(path)
113
+ # Expand user path (e.g., ~/ or $HOME)
114
+ expanded_path = os.path.expanduser(path)
115
+ resolved_path: Path = Path(expanded_path).resolve()
152
116
 
153
- # First, check if the path matches any excluded paths or patterns.
117
+ # Check exclusions first
154
118
  if self._is_path_excluded(resolved_path):
155
119
  return False
156
120
 
157
- # Check if the normalized path is within any allowed path.
121
+ # Check if the path is within any allowed path
158
122
  for allowed_path in self.allowed_paths:
159
123
  try:
160
- # relative_to will succeed if resolved_path is a subpath of allowed_path.
161
124
  resolved_path.relative_to(allowed_path)
162
125
  return True
163
126
  except ValueError:
@@ -166,74 +129,63 @@ class PermissionManager:
166
129
  return False
167
130
 
168
131
  def _is_path_excluded(self, path: Path) -> bool:
169
- """Determine if a normalized path should be excluded.
170
-
171
- The method checks two conditions:
172
- 1. If the path exactly matches an entry in the excluded_paths set.
173
- 2. If the path string contains any of the excluded patterns, either as a
174
- suffix for wildcard patterns (e.g., "*.log") or as an exact match
175
- within any of the path's components.
132
+ """Check if a path is excluded.
176
133
 
177
134
  Args:
178
- path: The normalized path to check.
135
+ path: The path to check
179
136
 
180
137
  Returns:
181
- True if the path is excluded, False otherwise.
138
+ True if the path is excluded, False otherwise
182
139
  """
183
- # Direct match: Check if the path is in the explicitly excluded paths set.
140
+
141
+ # Check exact excluded paths
184
142
  if path in self.excluded_paths:
185
143
  return True
186
144
 
187
- # Convert the path to a string for pattern matching.
145
+ # Check excluded patterns
188
146
  path_str: str = str(path)
189
147
 
190
- # Split the path into its individual components (directories and file name).
148
+ # Get path parts to check for exact directory/file name matches
191
149
  path_parts = path_str.split(os.sep)
192
150
 
193
- # Iterate over each exclusion pattern to see if it matches.
194
151
  for pattern in self.excluded_patterns:
195
- # If the pattern starts with a wildcard, perform a suffix match.
152
+ # Handle wildcard patterns (e.g., "*.log")
196
153
  if pattern.startswith("*"):
197
154
  if path_str.endswith(pattern[1:]):
198
155
  return True
199
156
  else:
200
- # For non-wildcard patterns, check if any path component exactly matches the pattern.
157
+ # For non-wildcard patterns, check if any path component matches exactly
201
158
  if pattern in path_parts:
202
159
  return True
203
160
 
204
161
  return False
205
162
 
206
163
  def to_json(self) -> str:
207
- """Serialize the permission manager's configuration to a JSON string.
208
-
209
- The JSON representation includes the allowed paths, excluded paths, and
210
- excluded patterns, which can be used to restore the configuration later.
164
+ """Convert the permission manager to a JSON string.
211
165
 
212
166
  Returns:
213
- A JSON string representing the current state of the permission manager.
167
+ A JSON string representation of the permission manager
214
168
  """
215
169
  data: dict[str, Any] = {
216
170
  "allowed_paths": [str(p) for p in self.allowed_paths],
217
171
  "excluded_paths": [str(p) for p in self.excluded_paths],
218
172
  "excluded_patterns": self.excluded_patterns,
219
173
  }
174
+
220
175
  return json.dumps(data)
221
176
 
222
177
  @classmethod
223
178
  def from_json(cls, json_str: str) -> "PermissionManager":
224
- """Create a PermissionManager instance from a JSON string.
225
-
226
- The JSON string should represent a configuration with allowed paths,
227
- excluded paths, and exclusion patterns. This method rehydrates the state
228
- accordingly.
179
+ """Create a permission manager from a JSON string.
229
180
 
230
181
  Args:
231
- json_str: The JSON string containing the permission manager configuration.
182
+ json_str: The JSON string
232
183
 
233
184
  Returns:
234
- A new PermissionManager instance with configuration loaded from the JSON string.
185
+ A new PermissionManager instance
235
186
  """
236
187
  data: dict[str, Any] = json.loads(json_str)
188
+
237
189
  manager = cls()
238
190
 
239
191
  for path in data.get("allowed_paths", []):
@@ -243,16 +195,12 @@ class PermissionManager:
243
195
  manager.exclude_path(path)
244
196
 
245
197
  manager.excluded_patterns = data.get("excluded_patterns", [])
198
+
246
199
  return manager
247
200
 
248
201
 
249
202
  class PermissibleOperation:
250
- """A decorator for operations that require permission checks.
251
-
252
- This decorator uses a PermissionManager instance to enforce that a given
253
- operation (e.g., read, write, execute) is permitted on a provided file system
254
- path before allowing the decorated function to execute.
255
- """
203
+ """A decorator for operations that require permission."""
256
204
 
257
205
  def __init__(
258
206
  self,
@@ -260,54 +208,50 @@ class PermissibleOperation:
260
208
  operation: str,
261
209
  get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = None,
262
210
  ) -> None:
263
- """Initialize the PermissibleOperation decorator.
211
+ """Initialize the permissible operation.
264
212
 
265
213
  Args:
266
- permission_manager: The PermissionManager instance used for permission checks.
267
- operation: A string representing the operation type (e.g., 'read', 'write').
268
- get_path_fn: Optional function to extract the file system path from the function's
269
- arguments. If not provided, defaults to using the first positional argument
270
- or the first value from keyword arguments.
214
+ permission_manager: The permission manager
215
+ operation: The operation type (read, write, execute, etc.)
216
+ get_path_fn: Optional function to extract the path from args and kwargs
271
217
  """
272
218
  self.permission_manager: PermissionManager = permission_manager
273
219
  self.operation: str = operation
274
- self.get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = get_path_fn
220
+ self.get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = (
221
+ get_path_fn
222
+ )
275
223
 
276
224
  def __call__(
277
225
  self, func: Callable[..., Awaitable[T]]
278
226
  ) -> Callable[..., Awaitable[T]]:
279
- """Decorate the function to enforce permission checks before execution.
280
-
281
- This method wraps the original asynchronous function, extracting a file system path
282
- from its arguments and using the PermissionManager to verify if the specified operation
283
- is allowed on that path. If permission is denied, a PermissionError is raised.
227
+ """Decorate the function.
284
228
 
285
229
  Args:
286
- func: The asynchronous function to decorate.
230
+ func: The function to decorate
287
231
 
288
232
  Returns:
289
- An asynchronous function that includes permission checks prior to calling the original function.
233
+ The decorated function
290
234
  """
235
+
291
236
  async def wrapper(*args: Any, **kwargs: Any) -> T:
292
- # Extract the file system path using the provided get_path_fn if available.
237
+ # Extract the path
293
238
  if self.get_path_fn:
239
+ # Pass args as a list and kwargs as a dict to the path function
294
240
  path = self.get_path_fn(list(args), kwargs)
295
241
  else:
296
- # Default extraction: use the first positional argument if available,
297
- # otherwise use the first keyword argument value.
242
+ # Default to first argument
298
243
  path = args[0] if args else next(iter(kwargs.values()), None)
299
244
 
300
- # Ensure that the extracted path is a string.
301
245
  if not isinstance(path, str):
302
- raise ValueError(f"Invalid path type: {type(path)}. Expected a string.")
246
+ raise ValueError(f"Invalid path type: {type(path)}")
303
247
 
304
- # Check if the operation is allowed on the specified path.
248
+ # Check permission
305
249
  if not self.permission_manager.is_path_allowed(path):
306
250
  raise PermissionError(
307
251
  f"Operation '{self.operation}' not allowed for path: {path}"
308
252
  )
309
253
 
310
- # Execute the original function if the permission check passes.
254
+ # Call the function
311
255
  return await func(*args, **kwargs)
312
256
 
313
257
  return wrapper