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,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
API Client Module
|
|
3
|
+
|
|
4
|
+
Handles HTTP communication with the server
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from typing import Dict
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
|
|
12
|
+
from ..core.client_id import get_machine_metadata
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class APIError(Exception):
|
|
16
|
+
"""API-related errors"""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PairingError(APIError):
|
|
22
|
+
"""Pairing verification errors"""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NetworkError(APIError):
|
|
28
|
+
"""Network connection errors"""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class APIClient:
|
|
34
|
+
"""API client class"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, timeout: float = 10.0):
|
|
37
|
+
"""
|
|
38
|
+
Initialize API client
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
timeout: Request timeout (seconds)
|
|
42
|
+
"""
|
|
43
|
+
self.timeout = aiohttp.ClientTimeout(total=timeout)
|
|
44
|
+
|
|
45
|
+
async def _verify_pairing_async(
|
|
46
|
+
self, server_url: str, verification_code: str, client_id: str, workspace_id: str
|
|
47
|
+
) -> Dict[str, any]:
|
|
48
|
+
"""
|
|
49
|
+
Asynchronously verify pairing information with server
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
server_url: Server URL
|
|
53
|
+
verification_code: Verification code
|
|
54
|
+
client_id: Client unique identifier
|
|
55
|
+
workspace_id: Workspace ID
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
dict: Server response data
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
NetworkError: Network connection failure
|
|
62
|
+
PairingError: Verification failure (invalid verification code, etc.)
|
|
63
|
+
APIError: Other API errors
|
|
64
|
+
"""
|
|
65
|
+
# Ensure URL format is correct
|
|
66
|
+
if not server_url.startswith(("http://", "https://")):
|
|
67
|
+
server_url = f"https://{server_url}"
|
|
68
|
+
|
|
69
|
+
# Remove trailing slash
|
|
70
|
+
server_url = server_url.rstrip("/")
|
|
71
|
+
|
|
72
|
+
# Build pairing endpoint
|
|
73
|
+
endpoint = f"{server_url}/api/v1/bridge/pair/verify_code"
|
|
74
|
+
|
|
75
|
+
# Request payload
|
|
76
|
+
payload = {"verification_code": verification_code, "client_id": client_id, "workspace_id": workspace_id}
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
|
80
|
+
async with session.post(endpoint, json=payload) as response:
|
|
81
|
+
# Get response data
|
|
82
|
+
try:
|
|
83
|
+
response_data = await response.json()
|
|
84
|
+
except Exception:
|
|
85
|
+
response_data = None
|
|
86
|
+
|
|
87
|
+
# Handle response status code
|
|
88
|
+
if response.status == 200:
|
|
89
|
+
return response_data
|
|
90
|
+
|
|
91
|
+
# Handle error responses
|
|
92
|
+
error_msg = "Pairing verification failed"
|
|
93
|
+
if response_data:
|
|
94
|
+
# Extract error message from response
|
|
95
|
+
if "message" in response_data:
|
|
96
|
+
error_msg = response_data["message"]
|
|
97
|
+
elif "error" in response_data:
|
|
98
|
+
error_msg = response_data["error"]
|
|
99
|
+
elif "data" in response_data and isinstance(response_data["data"], dict):
|
|
100
|
+
if "error" in response_data["data"]:
|
|
101
|
+
error_msg = response_data["data"]["error"]
|
|
102
|
+
|
|
103
|
+
# Raise appropriate error based on status code
|
|
104
|
+
if response.status == 404:
|
|
105
|
+
raise PairingError(f"Server pairing endpoint does not exist: {endpoint}")
|
|
106
|
+
elif response.status >= 500:
|
|
107
|
+
raise APIError(f"Server error: {error_msg}")
|
|
108
|
+
elif response.status in [400, 401]:
|
|
109
|
+
raise PairingError(error_msg)
|
|
110
|
+
else:
|
|
111
|
+
raise APIError(f"{error_msg} (HTTP {response.status})")
|
|
112
|
+
|
|
113
|
+
except aiohttp.ClientConnectorError as e:
|
|
114
|
+
raise NetworkError(f"Unable to connect to server: {server_url}") from e
|
|
115
|
+
except asyncio.TimeoutError as e:
|
|
116
|
+
raise NetworkError(f"Connection timeout: {server_url}") from e
|
|
117
|
+
except (PairingError, APIError):
|
|
118
|
+
# Re-raise known errors
|
|
119
|
+
raise
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise APIError(f"Request failed: {e}") from e
|
|
122
|
+
|
|
123
|
+
def verify_pairing(
|
|
124
|
+
self, server_url: str, verification_code: str, client_id: str, workspace_id: str
|
|
125
|
+
) -> Dict[str, any]:
|
|
126
|
+
"""
|
|
127
|
+
Verify pairing information with server (synchronous wrapper)
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
server_url: Server URL
|
|
131
|
+
verification_code: Verification code
|
|
132
|
+
client_id: Client unique identifier
|
|
133
|
+
workspace_id: Workspace ID
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
dict: Server response data
|
|
137
|
+
|
|
138
|
+
Raises:
|
|
139
|
+
NetworkError: Network connection failure
|
|
140
|
+
PairingError: Verification failure (invalid verification code, etc.)
|
|
141
|
+
APIError: Other API errors
|
|
142
|
+
"""
|
|
143
|
+
return asyncio.run(self._verify_pairing_async(server_url, verification_code, client_id, workspace_id))
|
|
144
|
+
|
|
145
|
+
async def _test_connection_async(self, server_url: str) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
Asynchronously test connection to server
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
server_url: Server URL
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
bool: True if connection successful, False otherwise
|
|
154
|
+
"""
|
|
155
|
+
# Ensure URL format is correct
|
|
156
|
+
if not server_url.startswith(("http://", "https://")):
|
|
157
|
+
server_url = f"https://{server_url}"
|
|
158
|
+
|
|
159
|
+
server_url = server_url.rstrip("/")
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
|
163
|
+
async with session.get(f"{server_url}/api/v1/bridge/health") as response:
|
|
164
|
+
return response.status == 200
|
|
165
|
+
except Exception:
|
|
166
|
+
return False
|
|
167
|
+
|
|
168
|
+
def test_connection(self, server_url: str) -> bool:
|
|
169
|
+
"""
|
|
170
|
+
Test connection to server (synchronous wrapper)
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
server_url: Server URL
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
bool: True if connection successful, False otherwise
|
|
177
|
+
"""
|
|
178
|
+
return asyncio.run(self._test_connection_async(server_url))
|
|
179
|
+
|
|
180
|
+
async def _send_client_info(
|
|
181
|
+
self, server_url: str, client_id: str, verification_code: str, workspace_id: str, tools: Dict[str, list]
|
|
182
|
+
) -> bool:
|
|
183
|
+
"""
|
|
184
|
+
Asynchronously send MCP tools information to server
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
server_url: Server URL
|
|
188
|
+
client_id: Client unique identifier
|
|
189
|
+
verification_code: Verification code
|
|
190
|
+
workspace_id: Workspace ID
|
|
191
|
+
tools: MCP tools list, format: {server_name: [tool_name, ...]}
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
bool: True if send successful
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
NetworkError: Network connection failure
|
|
198
|
+
APIError: API error
|
|
199
|
+
"""
|
|
200
|
+
# Ensure URL format is correct
|
|
201
|
+
if not server_url.startswith(("http://", "https://")):
|
|
202
|
+
server_url = f"https://{server_url}"
|
|
203
|
+
|
|
204
|
+
server_url = server_url.rstrip("/")
|
|
205
|
+
endpoint = f"{server_url}/api/v1/bridge/pair/client_info"
|
|
206
|
+
|
|
207
|
+
# Request payload
|
|
208
|
+
payload = {
|
|
209
|
+
"client_id": client_id,
|
|
210
|
+
"verification_code": verification_code,
|
|
211
|
+
"workspace_id": workspace_id,
|
|
212
|
+
"mcp_tools": tools,
|
|
213
|
+
"metadata": get_machine_metadata(),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
async with aiohttp.ClientSession(timeout=self.timeout) as session:
|
|
218
|
+
async with session.post(endpoint, json=payload) as response:
|
|
219
|
+
# Get response data
|
|
220
|
+
try:
|
|
221
|
+
response_data = await response.json()
|
|
222
|
+
except Exception:
|
|
223
|
+
response_data = None
|
|
224
|
+
|
|
225
|
+
# Handle response status code
|
|
226
|
+
if response.status == 200:
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
# Handle error responses
|
|
230
|
+
error_msg = "Failed to send MCP tools"
|
|
231
|
+
if response_data:
|
|
232
|
+
# Extract error message from response
|
|
233
|
+
if "message" in response_data:
|
|
234
|
+
error_msg = response_data["message"]
|
|
235
|
+
elif "error" in response_data:
|
|
236
|
+
error_msg = response_data["error"]
|
|
237
|
+
elif "data" in response_data and isinstance(response_data["data"], dict):
|
|
238
|
+
if "error" in response_data["data"]:
|
|
239
|
+
error_msg = response_data["data"]["error"]
|
|
240
|
+
|
|
241
|
+
# Raise appropriate error based on status code
|
|
242
|
+
if response.status == 404:
|
|
243
|
+
raise APIError(f"MCP tools endpoint does not exist: {endpoint}")
|
|
244
|
+
elif response.status >= 500:
|
|
245
|
+
raise APIError(f"Server error: {error_msg}")
|
|
246
|
+
else:
|
|
247
|
+
raise APIError(f"{error_msg} (HTTP {response.status})")
|
|
248
|
+
|
|
249
|
+
except aiohttp.ClientConnectorError as e:
|
|
250
|
+
raise NetworkError(f"Unable to connect to server: {server_url}") from e
|
|
251
|
+
except asyncio.TimeoutError as e:
|
|
252
|
+
raise NetworkError(f"Connection timeout: {server_url}") from e
|
|
253
|
+
except (APIError, NetworkError):
|
|
254
|
+
raise
|
|
255
|
+
except Exception as e:
|
|
256
|
+
raise APIError(f"Request failed: {e}") from e
|
|
257
|
+
|
|
258
|
+
def send_mcp_tools(self, server_url: str, client_id: str, verification_code: str, tools: list) -> bool:
|
|
259
|
+
"""
|
|
260
|
+
Send MCP tools information to server (synchronous wrapper)
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
server_url: Server URL
|
|
264
|
+
client_id: Client unique identifier
|
|
265
|
+
verification_code: Verification code
|
|
266
|
+
tools: MCP tools list
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
bool: True if send successful
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
NetworkError: Network connection failure
|
|
273
|
+
APIError: API error
|
|
274
|
+
"""
|
|
275
|
+
return asyncio.run(self._send_mcp_tools_async(server_url, client_id, verification_code, tools))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# Global API client instance
|
|
279
|
+
_api_client = APIClient()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_api_client() -> APIClient:
|
|
283
|
+
"""Get global API client instance"""
|
|
284
|
+
return _api_client
|