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,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command Whitelist Management Module
|
|
3
|
+
|
|
4
|
+
Responsible for loading and managing command whitelist, checking if commands are allowed to execute
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib.resources
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
from typing import List, Set, Tuple
|
|
12
|
+
|
|
13
|
+
# Configure logging
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# Whitelist configuration file path
|
|
17
|
+
WHITELIST_FILE_PATH = os.path.join(os.path.expanduser("~"), ".castrel", "whitelist.conf")
|
|
18
|
+
|
|
19
|
+
# Cache default whitelist (read from package data file, won't change, can be cached)
|
|
20
|
+
_default_whitelist_cache: List[str] = []
|
|
21
|
+
_default_whitelist_loaded: bool = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _load_default_whitelist() -> List[str]:
|
|
25
|
+
"""
|
|
26
|
+
Load default whitelist command list from package data file
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
List[str]: Default whitelist command list
|
|
30
|
+
"""
|
|
31
|
+
global _default_whitelist_cache, _default_whitelist_loaded
|
|
32
|
+
|
|
33
|
+
if _default_whitelist_loaded:
|
|
34
|
+
return _default_whitelist_cache
|
|
35
|
+
|
|
36
|
+
commands = []
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
# Use importlib.resources to read package data file
|
|
40
|
+
# Python 3.9+ recommends using files() API
|
|
41
|
+
data_files = importlib.resources.files("castrel_proxy.data")
|
|
42
|
+
whitelist_file = data_files.joinpath("default_whitelist.txt")
|
|
43
|
+
|
|
44
|
+
content = whitelist_file.read_text(encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
for line in content.splitlines():
|
|
47
|
+
line = line.strip()
|
|
48
|
+
# Skip empty lines and comment lines
|
|
49
|
+
if not line or line.startswith("#"):
|
|
50
|
+
continue
|
|
51
|
+
commands.append(line)
|
|
52
|
+
|
|
53
|
+
logger.debug(f"[WHITELIST] Loaded {len(commands)} default commands from package data")
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
logger.error(f"[WHITELIST] Failed to load default whitelist from package: {e}")
|
|
57
|
+
# If loading fails, return basic command list as fallback
|
|
58
|
+
commands = [
|
|
59
|
+
"ls",
|
|
60
|
+
"cat",
|
|
61
|
+
"head",
|
|
62
|
+
"tail",
|
|
63
|
+
"grep",
|
|
64
|
+
"find",
|
|
65
|
+
"pwd",
|
|
66
|
+
"cd",
|
|
67
|
+
"mkdir",
|
|
68
|
+
"echo",
|
|
69
|
+
"git",
|
|
70
|
+
"python",
|
|
71
|
+
"python3",
|
|
72
|
+
"pip",
|
|
73
|
+
"pip3",
|
|
74
|
+
"node",
|
|
75
|
+
"npm",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
_default_whitelist_cache = commands
|
|
79
|
+
_default_whitelist_loaded = True
|
|
80
|
+
|
|
81
|
+
return commands
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_default_whitelist() -> List[str]:
|
|
85
|
+
"""
|
|
86
|
+
Get default whitelist command list
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
List[str]: Default whitelist command list
|
|
90
|
+
"""
|
|
91
|
+
return _load_default_whitelist()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _ensure_whitelist_file_exists() -> None:
|
|
95
|
+
"""
|
|
96
|
+
Ensure whitelist configuration file exists, copy default config from package data file if not exists
|
|
97
|
+
"""
|
|
98
|
+
if os.path.exists(WHITELIST_FILE_PATH):
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
# Ensure directory exists
|
|
102
|
+
whitelist_dir = os.path.dirname(WHITELIST_FILE_PATH)
|
|
103
|
+
os.makedirs(whitelist_dir, exist_ok=True)
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
# Read default config from package data file
|
|
107
|
+
data_files = importlib.resources.files("castrel_proxy.data")
|
|
108
|
+
whitelist_file = data_files.joinpath("default_whitelist.txt")
|
|
109
|
+
content = whitelist_file.read_text(encoding="utf-8")
|
|
110
|
+
|
|
111
|
+
# Write to user configuration file
|
|
112
|
+
with open(WHITELIST_FILE_PATH, "w", encoding="utf-8") as f:
|
|
113
|
+
f.write(content)
|
|
114
|
+
|
|
115
|
+
logger.info(f"[WHITELIST] Created default whitelist file from package data: {WHITELIST_FILE_PATH}")
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.error(f"[WHITELIST] Failed to copy default whitelist from package: {e}")
|
|
119
|
+
# If reading from package fails, create using basic command list
|
|
120
|
+
basic_commands = get_default_whitelist()
|
|
121
|
+
content = "# Castrel Command Whitelist Configuration File\n"
|
|
122
|
+
content += "# One command name per line, lines starting with # are comments\n\n"
|
|
123
|
+
content += "\n".join(basic_commands) + "\n"
|
|
124
|
+
|
|
125
|
+
with open(WHITELIST_FILE_PATH, "w", encoding="utf-8") as f:
|
|
126
|
+
f.write(content)
|
|
127
|
+
|
|
128
|
+
logger.info(f"[WHITELIST] Created basic whitelist file: {WHITELIST_FILE_PATH}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def load_whitelist() -> Set[str]:
|
|
132
|
+
"""
|
|
133
|
+
Load whitelist configuration (re-read file on each call, supports dynamic modification)
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Set[str]: Whitelist command set
|
|
137
|
+
"""
|
|
138
|
+
# Ensure configuration file exists
|
|
139
|
+
_ensure_whitelist_file_exists()
|
|
140
|
+
|
|
141
|
+
whitelist: Set[str] = set()
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
with open(WHITELIST_FILE_PATH, "r", encoding="utf-8") as f:
|
|
145
|
+
for line in f:
|
|
146
|
+
# Remove leading and trailing whitespace
|
|
147
|
+
line = line.strip()
|
|
148
|
+
|
|
149
|
+
# Skip empty lines and comment lines
|
|
150
|
+
if not line or line.startswith("#"):
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
# Add to whitelist
|
|
154
|
+
whitelist.add(line)
|
|
155
|
+
|
|
156
|
+
logger.debug(f"[WHITELIST] Loaded {len(whitelist)} commands from whitelist: {WHITELIST_FILE_PATH}")
|
|
157
|
+
|
|
158
|
+
except Exception as e:
|
|
159
|
+
logger.error(f"[WHITELIST] Failed to load whitelist file: {e}")
|
|
160
|
+
# If loading fails, use default whitelist
|
|
161
|
+
whitelist = set(get_default_whitelist())
|
|
162
|
+
logger.warning(f"[WHITELIST] Using default whitelist with {len(whitelist)} commands")
|
|
163
|
+
|
|
164
|
+
return whitelist
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _get_base_command(command: str) -> str:
|
|
168
|
+
"""
|
|
169
|
+
Extract base command name from a single command
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
command: Single command string
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
str: Base command name
|
|
176
|
+
"""
|
|
177
|
+
if not command or not command.strip():
|
|
178
|
+
return ""
|
|
179
|
+
|
|
180
|
+
# Remove leading and trailing whitespace
|
|
181
|
+
command = command.strip()
|
|
182
|
+
|
|
183
|
+
# Skip environment variable assignment prefix (e.g., VAR=value cmd)
|
|
184
|
+
while "=" in command.split()[0] if command.split() else False:
|
|
185
|
+
parts = command.split(None, 1)
|
|
186
|
+
if len(parts) > 1:
|
|
187
|
+
command = parts[1]
|
|
188
|
+
else:
|
|
189
|
+
return ""
|
|
190
|
+
|
|
191
|
+
# Extract first word of command
|
|
192
|
+
parts = command.split()
|
|
193
|
+
if not parts:
|
|
194
|
+
return ""
|
|
195
|
+
|
|
196
|
+
base_command = parts[0]
|
|
197
|
+
|
|
198
|
+
# Handle path-style commands (e.g., /usr/bin/ls -> ls)
|
|
199
|
+
base_command = os.path.basename(base_command)
|
|
200
|
+
|
|
201
|
+
return base_command
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _remove_quoted_strings(command: str) -> str:
|
|
205
|
+
"""
|
|
206
|
+
Remove quoted content from command to avoid operators inside quotes being split
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
command: Command string
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
str: Command with quoted content removed
|
|
213
|
+
"""
|
|
214
|
+
result = []
|
|
215
|
+
i = 0
|
|
216
|
+
in_single_quote = False
|
|
217
|
+
in_double_quote = False
|
|
218
|
+
escape_next = False
|
|
219
|
+
|
|
220
|
+
while i < len(command):
|
|
221
|
+
char = command[i]
|
|
222
|
+
|
|
223
|
+
if escape_next:
|
|
224
|
+
escape_next = False
|
|
225
|
+
i += 1
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
if char == "\\":
|
|
229
|
+
escape_next = True
|
|
230
|
+
i += 1
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
if char == "'" and not in_double_quote:
|
|
234
|
+
in_single_quote = not in_single_quote
|
|
235
|
+
i += 1
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
if char == '"' and not in_single_quote:
|
|
239
|
+
in_double_quote = not in_double_quote
|
|
240
|
+
i += 1
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
if not in_single_quote and not in_double_quote:
|
|
244
|
+
result.append(char)
|
|
245
|
+
|
|
246
|
+
i += 1
|
|
247
|
+
|
|
248
|
+
return "".join(result)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _extract_command_substitutions(command: str) -> List[str]:
|
|
252
|
+
"""
|
|
253
|
+
Extract commands from command substitutions: $(...) and `...`
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
command: Command string
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
List[str]: List of commands from command substitutions
|
|
260
|
+
"""
|
|
261
|
+
substitutions = []
|
|
262
|
+
|
|
263
|
+
# Match $(...) - need to handle nested parentheses
|
|
264
|
+
# Simplified handling: use regex to match non-nested cases
|
|
265
|
+
dollar_paren_pattern = r"\$\(([^()]*)\)"
|
|
266
|
+
for match in re.finditer(dollar_paren_pattern, command):
|
|
267
|
+
inner_cmd = match.group(1).strip()
|
|
268
|
+
if inner_cmd:
|
|
269
|
+
substitutions.append(inner_cmd)
|
|
270
|
+
|
|
271
|
+
# Match `...` backticks
|
|
272
|
+
backtick_pattern = r"`([^`]*)`"
|
|
273
|
+
for match in re.finditer(backtick_pattern, command):
|
|
274
|
+
inner_cmd = match.group(1).strip()
|
|
275
|
+
if inner_cmd:
|
|
276
|
+
substitutions.append(inner_cmd)
|
|
277
|
+
|
|
278
|
+
return substitutions
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _extract_commands(full_command: str) -> List[str]:
|
|
282
|
+
"""
|
|
283
|
+
Extract all subcommands from compound command
|
|
284
|
+
|
|
285
|
+
Handled operators:
|
|
286
|
+
- Command separators: &&, ||, ;, & (background execution)
|
|
287
|
+
- Pipes: |, |&
|
|
288
|
+
- Command substitutions: $(...), `...`
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
full_command: Complete command string
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
List[str]: List of all subcommands
|
|
295
|
+
"""
|
|
296
|
+
if not full_command or not full_command.strip():
|
|
297
|
+
return []
|
|
298
|
+
|
|
299
|
+
commands = []
|
|
300
|
+
|
|
301
|
+
# First extract commands from command substitutions
|
|
302
|
+
substitutions = _extract_command_substitutions(full_command)
|
|
303
|
+
for sub in substitutions:
|
|
304
|
+
# Recursively extract subcommands from command substitutions
|
|
305
|
+
commands.extend(_extract_commands(sub))
|
|
306
|
+
|
|
307
|
+
# Remove quoted content to avoid operators inside quotes being split
|
|
308
|
+
cleaned_command = _remove_quoted_strings(full_command)
|
|
309
|
+
|
|
310
|
+
# Use regex to split commands
|
|
311
|
+
# Match: &&, ||, |&, |, ;, & (but not single characters in && or |&)
|
|
312
|
+
# Note order: match longer operators first
|
|
313
|
+
split_pattern = r"\s*(?:&&|\|\||;\s*|\|&|\||\s+&\s+|&\s*$)\s*"
|
|
314
|
+
|
|
315
|
+
# Split commands
|
|
316
|
+
parts = re.split(split_pattern, cleaned_command)
|
|
317
|
+
|
|
318
|
+
for part in parts:
|
|
319
|
+
part = part.strip()
|
|
320
|
+
if part:
|
|
321
|
+
# Remove redirection parts (but keep the command itself)
|
|
322
|
+
# Match: >, >>, <, <<, 2>, 2>>, &>, >&, etc.
|
|
323
|
+
# Only remove redirection symbols and following file paths
|
|
324
|
+
redirect_pattern = r"\s*(?:\d*>>?|<<?|&>|>&)\s*\S+"
|
|
325
|
+
part_no_redirect = re.sub(redirect_pattern, "", part).strip()
|
|
326
|
+
|
|
327
|
+
if part_no_redirect:
|
|
328
|
+
commands.append(part_no_redirect)
|
|
329
|
+
|
|
330
|
+
return commands
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def is_command_allowed(full_command: str) -> Tuple[bool, List[str]]:
|
|
334
|
+
"""
|
|
335
|
+
Check if command is in whitelist
|
|
336
|
+
|
|
337
|
+
Parse all subcommands in compound command, ensure each subcommand is in whitelist.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
full_command: Complete command string
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Tuple[bool, List[str]]: (Whether execution is allowed, List of commands not in whitelist)
|
|
344
|
+
"""
|
|
345
|
+
if not full_command or not full_command.strip():
|
|
346
|
+
return False, ["(empty command)"]
|
|
347
|
+
|
|
348
|
+
# Extract all subcommands
|
|
349
|
+
commands = _extract_commands(full_command)
|
|
350
|
+
|
|
351
|
+
if not commands:
|
|
352
|
+
# If no commands extracted, try to get base command directly
|
|
353
|
+
base_cmd = _get_base_command(full_command)
|
|
354
|
+
if base_cmd:
|
|
355
|
+
commands = [full_command]
|
|
356
|
+
else:
|
|
357
|
+
return False, ["(empty command)"]
|
|
358
|
+
|
|
359
|
+
# Load whitelist
|
|
360
|
+
whitelist = load_whitelist()
|
|
361
|
+
|
|
362
|
+
# Check each subcommand
|
|
363
|
+
blocked_commands = []
|
|
364
|
+
for cmd in commands:
|
|
365
|
+
base_cmd = _get_base_command(cmd)
|
|
366
|
+
if not base_cmd:
|
|
367
|
+
continue
|
|
368
|
+
|
|
369
|
+
if base_cmd not in whitelist:
|
|
370
|
+
blocked_commands.append(base_cmd)
|
|
371
|
+
|
|
372
|
+
is_allowed = len(blocked_commands) == 0
|
|
373
|
+
|
|
374
|
+
if not is_allowed:
|
|
375
|
+
logger.warning(
|
|
376
|
+
f"[WHITELIST] Commands not in whitelist: blocked={blocked_commands}, full_command={full_command[:200]}"
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
return is_allowed, blocked_commands
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def get_whitelist_file_path() -> str:
|
|
383
|
+
"""
|
|
384
|
+
Get whitelist configuration file path
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
str: Full path to whitelist configuration file
|
|
388
|
+
"""
|
|
389
|
+
return WHITELIST_FILE_PATH
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def init_whitelist_file() -> str:
|
|
393
|
+
"""
|
|
394
|
+
Initialize whitelist configuration file
|
|
395
|
+
|
|
396
|
+
If file does not exist, copy default config from package data file to user directory.
|
|
397
|
+
If file already exists, do nothing.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
str: Whitelist configuration file path
|
|
401
|
+
"""
|
|
402
|
+
_ensure_whitelist_file_exists()
|
|
403
|
+
return WHITELIST_FILE_PATH
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: castrel-proxy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight remote command execution bridge client with MCP integration
|
|
5
|
+
Project-URL: Homepage, https://github.com/castrel-ai/castrel-bridge-proxy
|
|
6
|
+
Project-URL: Documentation, https://github.com/castrel-ai/castrel-bridge-proxy#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/castrel-ai/castrel-bridge-proxy
|
|
8
|
+
Project-URL: Issues, https://github.com/castrel-ai/castrel-bridge-proxy/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/castrel-ai/castrel-bridge-proxy/blob/main/CHANGELOG.md
|
|
10
|
+
Author: Castrel Team
|
|
11
|
+
License: MIT License
|
|
12
|
+
|
|
13
|
+
Copyright (c) 2025 Cloudwise
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Keywords: bridge,mcp,proxy,remote-execution,websocket
|
|
34
|
+
Classifier: Development Status :: 3 - Alpha
|
|
35
|
+
Classifier: Intended Audience :: Developers
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
43
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
44
|
+
Classifier: Topic :: System :: Networking
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
|
+
Requires-Dist: aiohttp>=3.9.0
|
|
47
|
+
Requires-Dist: langchain-mcp-adapters>=0.2.1
|
|
48
|
+
Requires-Dist: mcp>=1.0.0
|
|
49
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
50
|
+
Requires-Dist: typer>=0.20.1
|
|
51
|
+
Provides-Extra: dev
|
|
52
|
+
Requires-Dist: black>=23.0.0; extra == 'dev'
|
|
53
|
+
Requires-Dist: flake8>=6.0.0; extra == 'dev'
|
|
54
|
+
Requires-Dist: isort>=5.12.0; extra == 'dev'
|
|
55
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
56
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
57
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
|
|
58
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
59
|
+
Description-Content-Type: text/markdown
|
|
60
|
+
|
|
61
|
+
# Castrel Bridge Proxy
|
|
62
|
+
|
|
63
|
+
[](https://github.com/castrel-ai/castrel-bridge-proxy/actions)
|
|
64
|
+
[](https://opensource.org/licenses/MIT)
|
|
65
|
+
[](https://pypi.org/project/castrel-proxy/)
|
|
66
|
+
|
|
67
|
+
A lightweight remote command execution bridge client that connects to a server via WebSocket to receive and execute commands, with MCP (Model Context Protocol) integration.
|
|
68
|
+
|
|
69
|
+
## ✨ Features
|
|
70
|
+
|
|
71
|
+
- ✅ **Secure Pairing**: Pair with server using verification codes
|
|
72
|
+
- ✅ **Persistent Configuration**: Configuration saved in `~/.castrel/config.yaml`
|
|
73
|
+
- ✅ **Unique Identifier**: Generate stable client ID based on machine characteristics
|
|
74
|
+
- ✅ **WebSocket Connection**: Real-time bidirectional communication
|
|
75
|
+
- ✅ **Command Execution**: Execute shell commands with whitelist security
|
|
76
|
+
- ✅ **Document Operations**: Read, write, and edit files remotely
|
|
77
|
+
- ✅ **Auto Reconnect**: Automatically reconnect when connection is lost
|
|
78
|
+
- ✅ **Timeout Control**: Command execution timeout protection
|
|
79
|
+
- ✅ **MCP Integration**: Connect to local MCP services and sync tools information
|
|
80
|
+
|
|
81
|
+
## 📦 Installation
|
|
82
|
+
|
|
83
|
+
### Via pip
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install castrel-proxy
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### From source
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
git clone https://github.com/castrel-ai/castrel-bridge-proxy.git
|
|
93
|
+
cd castrel-bridge-proxy
|
|
94
|
+
pip install -e .
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## 🚀 Quick Start
|
|
98
|
+
|
|
99
|
+
### 1. Pair with Server
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
castrel-proxy pair <verification_code> <server_url>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
```bash
|
|
107
|
+
castrel-proxy pair eyJ0cyI6MTczNTA4ODQwMCwid2lkIjoiZGVmYXVsdCIsInJhbmQiOiIxMjM0NTYifQ https://server.example.com
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 2. Start Bridge Service
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Run in background (default)
|
|
114
|
+
castrel-proxy start
|
|
115
|
+
|
|
116
|
+
# Run in foreground
|
|
117
|
+
castrel-proxy start --foreground
|
|
118
|
+
|
|
119
|
+
# Press Ctrl+C to stop (foreground mode only)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 3. Stop Bridge Service
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Stop background daemon
|
|
126
|
+
castrel-proxy stop
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 4. Check Status
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
castrel-proxy status
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 5. View Logs
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# View last 50 lines (default)
|
|
139
|
+
castrel-proxy logs
|
|
140
|
+
|
|
141
|
+
# View last 100 lines
|
|
142
|
+
castrel-proxy logs -n 100
|
|
143
|
+
|
|
144
|
+
# Follow logs in real-time
|
|
145
|
+
castrel-proxy logs -f
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## 📖 Documentation
|
|
149
|
+
|
|
150
|
+
- [Installation Guide](docs/installation.md)
|
|
151
|
+
- [Configuration Guide](docs/configuration.md)
|
|
152
|
+
- [Daemon Mode Guide](docs/daemon-mode.md)
|
|
153
|
+
- [MCP Integration](docs/mcp-integration.md)
|
|
154
|
+
- [API Reference](docs/api-reference.md)
|
|
155
|
+
- [Protocol Specification](docs/protocol.md)
|
|
156
|
+
- [Migration Guide](MIGRATION_GUIDE.md)
|
|
157
|
+
|
|
158
|
+
## 🔧 Configuration
|
|
159
|
+
|
|
160
|
+
### Bridge Configuration (`~/.castrel/config.yaml`)
|
|
161
|
+
|
|
162
|
+
Pairing information is saved automatically:
|
|
163
|
+
|
|
164
|
+
```yaml
|
|
165
|
+
server_url: "https://server.example.com"
|
|
166
|
+
verification_code: "ABC123"
|
|
167
|
+
client_id: "a1b2c3d4e5f6"
|
|
168
|
+
workspace_id: "default"
|
|
169
|
+
paired_at: "2025-12-22T10:30:00Z"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### MCP Configuration (`~/.castrel/mcp.json`)
|
|
173
|
+
|
|
174
|
+
Configure MCP services (optional):
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"mcpServers": {
|
|
179
|
+
"filesystem": {
|
|
180
|
+
"transport": "stdio",
|
|
181
|
+
"command": "npx",
|
|
182
|
+
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"],
|
|
183
|
+
"env": {}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
See `examples/mcp.json.example` for more examples.
|
|
190
|
+
|
|
191
|
+
### Command Whitelist (`~/.castrel/whitelist.conf`)
|
|
192
|
+
|
|
193
|
+
Configure allowed commands for security:
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
# Add commands one per line
|
|
197
|
+
ls
|
|
198
|
+
cat
|
|
199
|
+
git
|
|
200
|
+
python
|
|
201
|
+
# etc.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## 🏗️ Architecture
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
┌─────────────────────────────────────────────────┐
|
|
208
|
+
│ Bridge Client (Local) │
|
|
209
|
+
│ │
|
|
210
|
+
│ ┌──────────────────────────────────────────┐ │
|
|
211
|
+
│ │ CLI Commands │ │
|
|
212
|
+
│ └──────────────────────────────────────────┘ │
|
|
213
|
+
│ │ │
|
|
214
|
+
│ ┌─────────────┼─────────────┐ │
|
|
215
|
+
│ │ │ │ │
|
|
216
|
+
│ ┌───▼────┐ ┌────▼─────┐ ┌───▼─────┐ │
|
|
217
|
+
│ │ Core │ │ MCP │ │ Network │ │
|
|
218
|
+
│ │ Config │ │ Manager │ │ Client │ │
|
|
219
|
+
│ └────────┘ └──────────┘ └──────────┘ │
|
|
220
|
+
│ │ │ │
|
|
221
|
+
│ ┌─────▼─────┐ │ │
|
|
222
|
+
│ │ MCP │ │ │
|
|
223
|
+
│ │ Servers │ │ │
|
|
224
|
+
│ └───────────┘ ┌────▼─────┐ │
|
|
225
|
+
│ │ Command │ │
|
|
226
|
+
│ │ Executor │ │
|
|
227
|
+
│ └──────────┘ │
|
|
228
|
+
└─────────────────────────────────────────────────┘
|
|
229
|
+
│
|
|
230
|
+
│ WebSocket
|
|
231
|
+
▼
|
|
232
|
+
┌─────────────────────────────────────────────────┐
|
|
233
|
+
│ Bridge Server (Remote) │
|
|
234
|
+
│ /api/v1/bridge/ws?client_id=xxx&code=yyy │
|
|
235
|
+
└─────────────────────────────────────────────────┘
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## 🛠️ Development
|
|
239
|
+
|
|
240
|
+
### Setup Development Environment
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# Clone repository
|
|
244
|
+
git clone https://github.com/castrel-ai/castrel-bridge-proxy.git
|
|
245
|
+
cd castrel-bridge-proxy
|
|
246
|
+
|
|
247
|
+
# Create virtual environment
|
|
248
|
+
python -m venv venv
|
|
249
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
250
|
+
|
|
251
|
+
# Install dependencies
|
|
252
|
+
pip install -e ".[dev]"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Run Tests
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# Run all tests
|
|
259
|
+
pytest
|
|
260
|
+
|
|
261
|
+
# Run with coverage
|
|
262
|
+
pytest --cov=castrel_proxy
|
|
263
|
+
|
|
264
|
+
# Run specific test file
|
|
265
|
+
pytest tests/test_core.py
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Code Quality
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
# Format code
|
|
272
|
+
black src/
|
|
273
|
+
|
|
274
|
+
# Lint code
|
|
275
|
+
flake8 src/
|
|
276
|
+
|
|
277
|
+
# Type checking
|
|
278
|
+
mypy src/
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## 🤝 Contributing
|
|
282
|
+
|
|
283
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
284
|
+
|
|
285
|
+
## 📝 License
|
|
286
|
+
|
|
287
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
288
|
+
|
|
289
|
+
## 🔒 Security
|
|
290
|
+
|
|
291
|
+
For security concerns, please see [SECURITY.md](SECURITY.md) or contact security@example.com.
|
|
292
|
+
|
|
293
|
+
## 📮 Contact
|
|
294
|
+
|
|
295
|
+
- Issues: [GitHub Issues](https://github.com/castrel-ai/castrel-bridge-proxy/issues)
|
|
296
|
+
- Discussions: [GitHub Discussions](https://github.com/castrel-ai/castrel-bridge-proxy/discussions)
|
|
297
|
+
|
|
298
|
+
## 🙏 Acknowledgments
|
|
299
|
+
|
|
300
|
+
- Built with [Typer](https://typer.tiangolo.com/) for CLI
|
|
301
|
+
- Uses [aiohttp](https://docs.aiohttp.org/) for async WebSocket communication
|
|
302
|
+
- Integrates with [MCP](https://modelcontextprotocol.io/) for tool protocols
|