castrel-proxy 0.1.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.
- castrel_proxy/__init__.py +22 -0
- castrel_proxy/cli/__init__.py +5 -0
- castrel_proxy/cli/commands.py +608 -0
- castrel_proxy/core/__init__.py +18 -0
- castrel_proxy/core/client_id.py +94 -0
- castrel_proxy/core/config.py +158 -0
- castrel_proxy/core/daemon.py +206 -0
- castrel_proxy/core/executor.py +166 -0
- castrel_proxy/data/__init__.py +1 -0
- castrel_proxy/data/default_whitelist.txt +229 -0
- castrel_proxy/mcp/__init__.py +8 -0
- castrel_proxy/mcp/manager.py +278 -0
- castrel_proxy/network/__init__.py +13 -0
- castrel_proxy/network/api_client.py +284 -0
- castrel_proxy/network/websocket_client.py +1148 -0
- castrel_proxy/operations/__init__.py +17 -0
- castrel_proxy/operations/document.py +343 -0
- castrel_proxy/security/__init__.py +17 -0
- castrel_proxy/security/whitelist.py +403 -0
- castrel_proxy-0.1.0.dist-info/METADATA +302 -0
- castrel_proxy-0.1.0.dist-info/RECORD +24 -0
- castrel_proxy-0.1.0.dist-info/WHEEL +4 -0
- castrel_proxy-0.1.0.dist-info/entry_points.txt +2 -0
- castrel_proxy-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""File and document operations modules"""
|
|
2
|
+
|
|
3
|
+
from .document import (
|
|
4
|
+
DocumentOperationError,
|
|
5
|
+
edit_document,
|
|
6
|
+
parse_document_args,
|
|
7
|
+
read_document,
|
|
8
|
+
write_document,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"DocumentOperationError",
|
|
13
|
+
"read_document",
|
|
14
|
+
"write_document",
|
|
15
|
+
"edit_document",
|
|
16
|
+
"parse_document_args",
|
|
17
|
+
]
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Document Operations Module
|
|
3
|
+
|
|
4
|
+
Provides document reading, writing, and editing functionality
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
# File size limit (10MB)
|
|
15
|
+
MAX_FILE_SIZE = 10 * 1024 * 1024
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DocumentOperationError(Exception):
|
|
19
|
+
"""Document operation exceptions"""
|
|
20
|
+
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _expand_path(file_path: str) -> Path:
|
|
25
|
+
"""
|
|
26
|
+
Expand file path, supports ~ and environment variables
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
file_path: Original file path
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Path: Expanded path object
|
|
33
|
+
"""
|
|
34
|
+
expanded = os.path.expanduser(os.path.expandvars(file_path))
|
|
35
|
+
return Path(expanded).resolve()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _validate_path(file_path: Path) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Validate path security
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
file_path: File path
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
DocumentOperationError: Raised when path is unsafe
|
|
47
|
+
"""
|
|
48
|
+
# Check if path is absolute
|
|
49
|
+
if not file_path.is_absolute():
|
|
50
|
+
raise DocumentOperationError(f"Path must be absolute: {file_path}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _detect_encoding(file_path: Path) -> str:
|
|
54
|
+
"""
|
|
55
|
+
Detect file encoding
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
file_path: File path
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
str: Encoding name
|
|
62
|
+
"""
|
|
63
|
+
# Try common encodings
|
|
64
|
+
encodings = ["utf-8", "gbk", "gb2312", "latin-1"]
|
|
65
|
+
|
|
66
|
+
for encoding in encodings:
|
|
67
|
+
try:
|
|
68
|
+
with open(file_path, "r", encoding=encoding) as f:
|
|
69
|
+
f.read()
|
|
70
|
+
logger.debug(f"Detected encoding: {encoding} for {file_path}")
|
|
71
|
+
return encoding
|
|
72
|
+
except (UnicodeDecodeError, LookupError):
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
# Default to utf-8
|
|
76
|
+
logger.warning(f"Could not detect encoding for {file_path}, using utf-8")
|
|
77
|
+
return "utf-8"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def read_document(file_path: str, encoding: Optional[str] = None) -> Dict[str, Any]:
|
|
81
|
+
"""
|
|
82
|
+
Read document content
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
file_path: File path
|
|
86
|
+
encoding: File encoding, defaults to auto-detect
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Dict[str, Any]: Execution result
|
|
90
|
+
{
|
|
91
|
+
"success": bool,
|
|
92
|
+
"content": str, # File content
|
|
93
|
+
"encoding": str, # Encoding used
|
|
94
|
+
"size": int, # File size (bytes)
|
|
95
|
+
"error": str # Error message (if failed)
|
|
96
|
+
}
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
# Expand path
|
|
100
|
+
path = _expand_path(file_path)
|
|
101
|
+
logger.info(f"[DOC-READ] Reading document: {path}")
|
|
102
|
+
|
|
103
|
+
# Validate path
|
|
104
|
+
_validate_path(path)
|
|
105
|
+
|
|
106
|
+
# Check if file exists
|
|
107
|
+
if not path.exists():
|
|
108
|
+
return {"success": False, "error": f"File does not exist: {path}"}
|
|
109
|
+
|
|
110
|
+
# Check if it's a file
|
|
111
|
+
if not path.is_file():
|
|
112
|
+
return {"success": False, "error": f"Not a file: {path}"}
|
|
113
|
+
|
|
114
|
+
# Check file size
|
|
115
|
+
file_size = path.stat().st_size
|
|
116
|
+
if file_size > MAX_FILE_SIZE:
|
|
117
|
+
return {
|
|
118
|
+
"success": False,
|
|
119
|
+
"error": f"File too large: {file_size} bytes (max: {MAX_FILE_SIZE} bytes)",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Check read permission
|
|
123
|
+
if not os.access(path, os.R_OK):
|
|
124
|
+
return {"success": False, "error": f"No read permission: {path}"}
|
|
125
|
+
|
|
126
|
+
# Detect encoding
|
|
127
|
+
if encoding is None:
|
|
128
|
+
encoding = _detect_encoding(path)
|
|
129
|
+
|
|
130
|
+
# Read file
|
|
131
|
+
try:
|
|
132
|
+
with open(path, "r", encoding=encoding) as f:
|
|
133
|
+
content = f.read()
|
|
134
|
+
except UnicodeDecodeError:
|
|
135
|
+
logger.warning(f"Failed to decode with {encoding}, trying utf-8 with errors='replace'")
|
|
136
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
137
|
+
content = f.read()
|
|
138
|
+
encoding = "utf-8 (with replacements)"
|
|
139
|
+
|
|
140
|
+
logger.info(f"[DOC-READ-SUCCESS] Read {file_size} bytes from {path}")
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
"success": True,
|
|
144
|
+
"content": content,
|
|
145
|
+
"encoding": encoding,
|
|
146
|
+
"size": file_size,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
except DocumentOperationError as e:
|
|
150
|
+
logger.error(f"[DOC-READ-ERROR] Validation error: {e}")
|
|
151
|
+
return {"success": False, "error": str(e)}
|
|
152
|
+
except Exception as e:
|
|
153
|
+
logger.error(f"[DOC-READ-ERROR] Unexpected error: {e}", exc_info=True)
|
|
154
|
+
return {"success": False, "error": f"Failed to read file: {str(e)}"}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def write_document(file_path: str, content: str, encoding: str = "utf-8", create_dirs: bool = True) -> Dict[str, Any]:
|
|
158
|
+
"""
|
|
159
|
+
Write document content (overwrite mode)
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
file_path: File path
|
|
163
|
+
content: File content
|
|
164
|
+
encoding: File encoding, defaults to utf-8
|
|
165
|
+
create_dirs: Whether to automatically create parent directories, defaults to True
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Dict[str, Any]: Execution result
|
|
169
|
+
{
|
|
170
|
+
"success": bool,
|
|
171
|
+
"size": int, # Number of bytes written
|
|
172
|
+
"path": str, # File path
|
|
173
|
+
"error": str # Error message (if failed)
|
|
174
|
+
}
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
# Expand path
|
|
178
|
+
path = _expand_path(file_path)
|
|
179
|
+
logger.info(f"[DOC-WRITE] Writing document: {path}")
|
|
180
|
+
|
|
181
|
+
# Validate path
|
|
182
|
+
_validate_path(path)
|
|
183
|
+
|
|
184
|
+
# Create parent directories
|
|
185
|
+
if create_dirs:
|
|
186
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
elif not path.parent.exists():
|
|
188
|
+
return {"success": False, "error": f"Parent directory does not exist: {path.parent}"}
|
|
189
|
+
|
|
190
|
+
# Check parent directory write permission
|
|
191
|
+
if not os.access(path.parent, os.W_OK):
|
|
192
|
+
return {"success": False, "error": f"No write permission: {path.parent}"}
|
|
193
|
+
|
|
194
|
+
# If file exists, check write permission
|
|
195
|
+
if path.exists() and not os.access(path, os.W_OK):
|
|
196
|
+
return {"success": False, "error": f"No write permission: {path}"}
|
|
197
|
+
|
|
198
|
+
# Write file
|
|
199
|
+
with open(path, "w", encoding=encoding) as f:
|
|
200
|
+
f.write(content)
|
|
201
|
+
|
|
202
|
+
# Get file size after writing
|
|
203
|
+
file_size = path.stat().st_size
|
|
204
|
+
|
|
205
|
+
logger.info(f"[DOC-WRITE-SUCCESS] Wrote {file_size} bytes to {path}")
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
"success": True,
|
|
209
|
+
"size": file_size,
|
|
210
|
+
"path": str(path),
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
except DocumentOperationError as e:
|
|
214
|
+
logger.error(f"[DOC-WRITE-ERROR] Validation error: {e}")
|
|
215
|
+
return {"success": False, "error": str(e)}
|
|
216
|
+
except Exception as e:
|
|
217
|
+
logger.error(f"[DOC-WRITE-ERROR] Unexpected error: {e}", exc_info=True)
|
|
218
|
+
return {"success": False, "error": f"Failed to write file: {str(e)}"}
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def edit_document(
|
|
222
|
+
file_path: str,
|
|
223
|
+
operation: str,
|
|
224
|
+
new_content: str,
|
|
225
|
+
old_content: Optional[str] = None,
|
|
226
|
+
encoding: Optional[str] = None,
|
|
227
|
+
) -> Dict[str, Any]:
|
|
228
|
+
"""
|
|
229
|
+
Edit document content
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
file_path: File path
|
|
233
|
+
operation: Operation type - "replace" (replace), "append" (append), "prepend" (prepend)
|
|
234
|
+
new_content: New content
|
|
235
|
+
old_content: Old content (only needed for replace operation)
|
|
236
|
+
encoding: File encoding, defaults to auto-detect
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Dict[str, Any]: Execution result
|
|
240
|
+
{
|
|
241
|
+
"success": bool,
|
|
242
|
+
"size": int, # File size after editing
|
|
243
|
+
"operation": str, # Operation performed
|
|
244
|
+
"path": str, # File path
|
|
245
|
+
"error": str # Error message (if failed)
|
|
246
|
+
}
|
|
247
|
+
"""
|
|
248
|
+
try:
|
|
249
|
+
# Expand path
|
|
250
|
+
path = _expand_path(file_path)
|
|
251
|
+
logger.info(f"[DOC-EDIT] Editing document: {path}, operation: {operation}")
|
|
252
|
+
|
|
253
|
+
# Validate path
|
|
254
|
+
_validate_path(path)
|
|
255
|
+
|
|
256
|
+
# Check operation type
|
|
257
|
+
if operation not in ["replace", "append", "prepend"]:
|
|
258
|
+
return {"success": False, "error": f"Unsupported operation type: {operation}"}
|
|
259
|
+
|
|
260
|
+
# Check if file exists
|
|
261
|
+
if not path.exists():
|
|
262
|
+
return {"success": False, "error": f"File does not exist: {path}"}
|
|
263
|
+
|
|
264
|
+
# Check if it's a file
|
|
265
|
+
if not path.is_file():
|
|
266
|
+
return {"success": False, "error": f"Not a file: {path}"}
|
|
267
|
+
|
|
268
|
+
# Check read/write permissions
|
|
269
|
+
if not os.access(path, os.R_OK):
|
|
270
|
+
return {"success": False, "error": f"No read permission: {path}"}
|
|
271
|
+
if not os.access(path, os.W_OK):
|
|
272
|
+
return {"success": False, "error": f"No write permission: {path}"}
|
|
273
|
+
|
|
274
|
+
# Read existing content
|
|
275
|
+
read_result = read_document(str(path), encoding)
|
|
276
|
+
if not read_result["success"]:
|
|
277
|
+
return read_result
|
|
278
|
+
|
|
279
|
+
current_content = read_result["content"]
|
|
280
|
+
detected_encoding = read_result["encoding"]
|
|
281
|
+
|
|
282
|
+
# Perform edit operation
|
|
283
|
+
if operation == "replace":
|
|
284
|
+
if old_content is None:
|
|
285
|
+
return {"success": False, "error": "replace operation requires old_content parameter"}
|
|
286
|
+
|
|
287
|
+
if old_content not in current_content:
|
|
288
|
+
return {"success": False, "error": f"Content to replace not found: {old_content[:50]}..."}
|
|
289
|
+
|
|
290
|
+
# Replace content
|
|
291
|
+
new_file_content = current_content.replace(old_content, new_content)
|
|
292
|
+
|
|
293
|
+
elif operation == "append":
|
|
294
|
+
# Append to end of file
|
|
295
|
+
new_file_content = current_content + new_content
|
|
296
|
+
|
|
297
|
+
elif operation == "prepend":
|
|
298
|
+
# Insert at beginning of file
|
|
299
|
+
new_file_content = new_content + current_content
|
|
300
|
+
|
|
301
|
+
# Write edited content
|
|
302
|
+
write_result = write_document(str(path), new_file_content, detected_encoding.split()[0], create_dirs=False)
|
|
303
|
+
|
|
304
|
+
if write_result["success"]:
|
|
305
|
+
write_result["operation"] = operation
|
|
306
|
+
logger.info(f"[DOC-EDIT-SUCCESS] Edited {path} with {operation} operation")
|
|
307
|
+
|
|
308
|
+
return write_result
|
|
309
|
+
|
|
310
|
+
except DocumentOperationError as e:
|
|
311
|
+
logger.error(f"[DOC-EDIT-ERROR] Validation error: {e}")
|
|
312
|
+
return {"success": False, "error": str(e)}
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.error(f"[DOC-EDIT-ERROR] Unexpected error: {e}", exc_info=True)
|
|
315
|
+
return {"success": False, "error": f"Failed to edit file: {str(e)}"}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def parse_document_args(args: list) -> Dict[str, Any]:
|
|
319
|
+
"""
|
|
320
|
+
Parse document operation arguments
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
args: Argument list, format like ["--file", "/path/to/file", "--content", "..."]
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Dict[str, Any]: Parsed arguments dictionary
|
|
327
|
+
"""
|
|
328
|
+
params = {}
|
|
329
|
+
i = 0
|
|
330
|
+
while i < len(args):
|
|
331
|
+
arg = args[i]
|
|
332
|
+
if arg.startswith("--"):
|
|
333
|
+
key = arg[2:] # Remove "--" prefix
|
|
334
|
+
if i + 1 < len(args) and not args[i + 1].startswith("--"):
|
|
335
|
+
params[key] = args[i + 1]
|
|
336
|
+
i += 2
|
|
337
|
+
else:
|
|
338
|
+
params[key] = True
|
|
339
|
+
i += 1
|
|
340
|
+
else:
|
|
341
|
+
i += 1
|
|
342
|
+
|
|
343
|
+
return params
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Security features for Castrel Bridge Proxy"""
|
|
2
|
+
|
|
3
|
+
from .whitelist import (
|
|
4
|
+
get_default_whitelist,
|
|
5
|
+
get_whitelist_file_path,
|
|
6
|
+
init_whitelist_file,
|
|
7
|
+
is_command_allowed,
|
|
8
|
+
load_whitelist,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"get_default_whitelist",
|
|
13
|
+
"load_whitelist",
|
|
14
|
+
"is_command_allowed",
|
|
15
|
+
"get_whitelist_file_path",
|
|
16
|
+
"init_whitelist_file",
|
|
17
|
+
]
|