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.
- hanzo_mcp/__init__.py +2 -2
- hanzo_mcp/cli.py +80 -9
- hanzo_mcp/server.py +41 -10
- hanzo_mcp/tools/__init__.py +51 -32
- hanzo_mcp/tools/agent/__init__.py +59 -0
- hanzo_mcp/tools/agent/agent_tool.py +474 -0
- hanzo_mcp/tools/agent/prompt.py +137 -0
- hanzo_mcp/tools/agent/tool_adapter.py +75 -0
- hanzo_mcp/tools/common/__init__.py +17 -0
- hanzo_mcp/tools/common/base.py +216 -0
- hanzo_mcp/tools/common/context.py +7 -3
- hanzo_mcp/tools/common/permissions.py +63 -119
- hanzo_mcp/tools/common/session.py +91 -0
- hanzo_mcp/tools/common/thinking_tool.py +123 -0
- hanzo_mcp/tools/filesystem/__init__.py +85 -5
- hanzo_mcp/tools/filesystem/base.py +113 -0
- hanzo_mcp/tools/filesystem/content_replace.py +287 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +286 -0
- hanzo_mcp/tools/filesystem/edit_file.py +287 -0
- hanzo_mcp/tools/filesystem/get_file_info.py +170 -0
- hanzo_mcp/tools/filesystem/read_files.py +198 -0
- hanzo_mcp/tools/filesystem/search_content.py +275 -0
- hanzo_mcp/tools/filesystem/write_file.py +162 -0
- hanzo_mcp/tools/jupyter/__init__.py +67 -4
- hanzo_mcp/tools/jupyter/base.py +284 -0
- hanzo_mcp/tools/jupyter/edit_notebook.py +295 -0
- hanzo_mcp/tools/jupyter/notebook_operations.py +72 -112
- hanzo_mcp/tools/jupyter/read_notebook.py +165 -0
- hanzo_mcp/tools/project/__init__.py +64 -1
- hanzo_mcp/tools/project/analysis.py +9 -6
- hanzo_mcp/tools/project/base.py +66 -0
- hanzo_mcp/tools/project/project_analyze.py +173 -0
- hanzo_mcp/tools/shell/__init__.py +58 -1
- hanzo_mcp/tools/shell/base.py +148 -0
- hanzo_mcp/tools/shell/command_executor.py +203 -322
- hanzo_mcp/tools/shell/run_command.py +204 -0
- hanzo_mcp/tools/shell/run_script.py +215 -0
- hanzo_mcp/tools/shell/script_tool.py +244 -0
- {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/METADATA +72 -77
- hanzo_mcp-0.1.30.dist-info/RECORD +45 -0
- {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/licenses/LICENSE +2 -2
- hanzo_mcp/tools/common/thinking.py +0 -65
- hanzo_mcp/tools/filesystem/file_operations.py +0 -1050
- hanzo_mcp-0.1.25.dist-info/RECORD +0 -24
- hanzo_mcp-0.1.25.dist-info/zip-safe +0 -1
- {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.30.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
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
|
|
67
|
+
"""Add a path to the allowed paths.
|
|
104
68
|
|
|
105
69
|
Args:
|
|
106
|
-
path: The
|
|
70
|
+
path: The path to allow
|
|
107
71
|
"""
|
|
108
|
-
|
|
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
|
|
81
|
+
path: The path to remove
|
|
116
82
|
"""
|
|
117
|
-
resolved_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
|
-
"""
|
|
88
|
+
"""Exclude a path from allowed operations.
|
|
123
89
|
|
|
124
90
|
Args:
|
|
125
|
-
path: The
|
|
91
|
+
path: The path to exclude
|
|
126
92
|
"""
|
|
127
|
-
resolved_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
|
|
97
|
+
"""Add an exclusion pattern.
|
|
132
98
|
|
|
133
99
|
Args:
|
|
134
|
-
pattern:
|
|
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
|
-
"""
|
|
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
|
|
108
|
+
path: The path to check
|
|
147
109
|
|
|
148
110
|
Returns:
|
|
149
|
-
True if the path is allowed
|
|
111
|
+
True if the path is allowed, False otherwise
|
|
150
112
|
"""
|
|
151
|
-
|
|
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
|
-
#
|
|
117
|
+
# Check exclusions first
|
|
154
118
|
if self._is_path_excluded(resolved_path):
|
|
155
119
|
return False
|
|
156
120
|
|
|
157
|
-
# Check if the
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
140
|
+
|
|
141
|
+
# Check exact excluded paths
|
|
184
142
|
if path in self.excluded_paths:
|
|
185
143
|
return True
|
|
186
144
|
|
|
187
|
-
#
|
|
145
|
+
# Check excluded patterns
|
|
188
146
|
path_str: str = str(path)
|
|
189
147
|
|
|
190
|
-
#
|
|
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
|
-
#
|
|
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
|
|
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
|
-
"""
|
|
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
|
|
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
|
|
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
|
|
182
|
+
json_str: The JSON string
|
|
232
183
|
|
|
233
184
|
Returns:
|
|
234
|
-
A new PermissionManager instance
|
|
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
|
|
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
|
|
211
|
+
"""Initialize the permissible operation.
|
|
264
212
|
|
|
265
213
|
Args:
|
|
266
|
-
permission_manager: The
|
|
267
|
-
operation:
|
|
268
|
-
get_path_fn: Optional function to extract the
|
|
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 =
|
|
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
|
|
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
|
|
230
|
+
func: The function to decorate
|
|
287
231
|
|
|
288
232
|
Returns:
|
|
289
|
-
|
|
233
|
+
The decorated function
|
|
290
234
|
"""
|
|
235
|
+
|
|
291
236
|
async def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
292
|
-
# Extract the
|
|
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
|
|
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)}
|
|
246
|
+
raise ValueError(f"Invalid path type: {type(path)}")
|
|
303
247
|
|
|
304
|
-
# Check
|
|
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
|
-
#
|
|
254
|
+
# Call the function
|
|
311
255
|
return await func(*args, **kwargs)
|
|
312
256
|
|
|
313
257
|
return wrapper
|