agentscope-runtime 1.0.4__py3-none-any.whl → 1.0.4a1__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.
- agentscope_runtime/adapters/agentscope/stream.py +7 -1
- agentscope_runtime/cli/commands/deploy.py +0 -371
- agentscope_runtime/engine/__init__.py +0 -4
- agentscope_runtime/engine/constant.py +0 -1
- agentscope_runtime/engine/deployers/__init__.py +0 -12
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +51 -26
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +10 -19
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +201 -4
- agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +25 -134
- agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
- agentscope_runtime/engine/runner.py +0 -12
- agentscope_runtime/engine/tracing/wrapper.py +4 -18
- agentscope_runtime/sandbox/__init__.py +6 -14
- agentscope_runtime/sandbox/box/base/__init__.py +2 -2
- agentscope_runtime/sandbox/box/base/base_sandbox.py +1 -51
- agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +2 -198
- agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +2 -99
- agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
- agentscope_runtime/sandbox/box/gui/gui_sandbox.py +1 -117
- agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
- agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +100 -247
- agentscope_runtime/sandbox/box/sandbox.py +65 -98
- agentscope_runtime/sandbox/box/shared/routers/generic.py +29 -36
- agentscope_runtime/sandbox/client/__init__.py +1 -6
- agentscope_runtime/sandbox/client/http_client.py +329 -108
- agentscope_runtime/sandbox/enums.py +0 -7
- agentscope_runtime/sandbox/manager/sandbox_manager.py +4 -264
- agentscope_runtime/sandbox/manager/server/app.py +1 -7
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-1.0.4.dist-info → agentscope_runtime-1.0.4a1.dist-info}/METADATA +28 -102
- {agentscope_runtime-1.0.4.dist-info → agentscope_runtime-1.0.4a1.dist-info}/RECORD +37 -46
- agentscope_runtime/adapters/ms_agent_framework/__init__.py +0 -0
- agentscope_runtime/adapters/ms_agent_framework/message.py +0 -205
- agentscope_runtime/adapters/ms_agent_framework/stream.py +0 -418
- agentscope_runtime/adapters/utils.py +0 -6
- agentscope_runtime/common/container_clients/knative_client.py +0 -466
- agentscope_runtime/engine/deployers/fc_deployer.py +0 -1506
- agentscope_runtime/engine/deployers/knative_deployer.py +0 -290
- agentscope_runtime/sandbox/client/async_http_client.py +0 -339
- agentscope_runtime/sandbox/client/base.py +0 -74
- {agentscope_runtime-1.0.4.dist-info → agentscope_runtime-1.0.4a1.dist-info}/WHEEL +0 -0
- {agentscope_runtime-1.0.4.dist-info → agentscope_runtime-1.0.4a1.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-1.0.4.dist-info → agentscope_runtime-1.0.4a1.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-1.0.4.dist-info → agentscope_runtime-1.0.4a1.dist-info}/top_level.txt +0 -0
|
@@ -5,7 +5,7 @@ import platform
|
|
|
5
5
|
from typing import Optional, List, Union
|
|
6
6
|
from urllib.parse import urlencode, urljoin
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from agentscope_runtime.sandbox.box.sandbox import Sandbox
|
|
9
9
|
|
|
10
10
|
from ...utils import build_image_uri
|
|
11
11
|
from ...registry import SandboxRegistry
|
|
@@ -15,107 +15,6 @@ from ...constant import TIMEOUT
|
|
|
15
15
|
logger = logging.getLogger(__name__)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
class HostPrerequisiteError(Exception):
|
|
19
|
-
"""Exception raised when host prerequisites
|
|
20
|
-
for MobileSandbox are not met."""
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _check_host_readiness() -> None:
|
|
24
|
-
logger.info(
|
|
25
|
-
"Performing one-time host environment check for MobileSandbox...",
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
architecture = platform.machine().lower()
|
|
29
|
-
if architecture in ("aarch64", "arm64"):
|
|
30
|
-
logger.warning(
|
|
31
|
-
"\n======================== WARNING ========================\n"
|
|
32
|
-
"ARM64/aarch64 architecture detected (e.g., Apple M-series).\n"
|
|
33
|
-
"Running this mobile sandbox on a non-x86_64 host may lead \n"
|
|
34
|
-
" to unexpected compatibility or performance issues.\n"
|
|
35
|
-
"=========================================================",
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
os_type = platform.system()
|
|
39
|
-
if os_type == "Linux":
|
|
40
|
-
try:
|
|
41
|
-
result = subprocess.run(
|
|
42
|
-
["lsmod"],
|
|
43
|
-
capture_output=True,
|
|
44
|
-
text=True,
|
|
45
|
-
check=True,
|
|
46
|
-
)
|
|
47
|
-
loaded_modules = result.stdout
|
|
48
|
-
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
49
|
-
loaded_modules = ""
|
|
50
|
-
logger.warning(
|
|
51
|
-
"Could not execute 'lsmod' to verify kernel modules.",
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
if "binder_linux" not in loaded_modules:
|
|
55
|
-
error_message = (
|
|
56
|
-
"\n========== HOST PREREQUISITE FAILED ==========\n"
|
|
57
|
-
"MobileSandbox requires specific kernel modules"
|
|
58
|
-
" that appear to be missing or not loaded.\n\n"
|
|
59
|
-
"To fix this, please run the following commands"
|
|
60
|
-
" on your Linux host:\n\n"
|
|
61
|
-
"## Install required kernel modules\n"
|
|
62
|
-
"sudo apt update"
|
|
63
|
-
" && sudo apt install -y linux-modules-extra-`uname -r`\n"
|
|
64
|
-
"sudo modprobe binder_linux"
|
|
65
|
-
' devices="binder,hwbinder,vndbinder"\n'
|
|
66
|
-
"## (Optional) Load the ashmem driver for older kernels\n"
|
|
67
|
-
"sudo modprobe ashmem_linux\n"
|
|
68
|
-
"=================================================="
|
|
69
|
-
)
|
|
70
|
-
raise HostPrerequisiteError(error_message)
|
|
71
|
-
|
|
72
|
-
if os_type == "Windows":
|
|
73
|
-
try:
|
|
74
|
-
result = subprocess.run(
|
|
75
|
-
["wsl", "lsmod"],
|
|
76
|
-
capture_output=True,
|
|
77
|
-
text=True,
|
|
78
|
-
check=True,
|
|
79
|
-
encoding="utf-8",
|
|
80
|
-
)
|
|
81
|
-
loaded_modules = result.stdout
|
|
82
|
-
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
83
|
-
loaded_modules = ""
|
|
84
|
-
logger.warning(
|
|
85
|
-
"Could not execute 'wsl lsmod' to verify kernel modules.",
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
if "binder_linux" not in loaded_modules:
|
|
89
|
-
error_message = (
|
|
90
|
-
"\n========== HOST PREREQUISITE FAILED ==========\n"
|
|
91
|
-
"MobileSandbox on Windows requires Docker Desktop "
|
|
92
|
-
"with the WSL 2 backend.\n"
|
|
93
|
-
"The required kernel modules seem to be missing "
|
|
94
|
-
"in your WSL 2 environment.\n\n"
|
|
95
|
-
"To fix this, please follow these steps:\n\n"
|
|
96
|
-
"1. **Ensure Docker Desktop is using WSL 2**:\n"
|
|
97
|
-
" - Open Docker Desktop -> Settings -> General.\n"
|
|
98
|
-
" - Make sure 'Use the WSL 2 based engine' "
|
|
99
|
-
"is checked.\n\n"
|
|
100
|
-
"2. **Ensure WSL is installed and updated**:\n"
|
|
101
|
-
" - Open PowerShell or Command Prompt "
|
|
102
|
-
"as Administrator.\n"
|
|
103
|
-
" - Run: wsl --install\n"
|
|
104
|
-
" - Run: wsl --update\n"
|
|
105
|
-
" (An update usually installs a recent Linux kernel "
|
|
106
|
-
"with the required modules.)\n\n"
|
|
107
|
-
"3. **Verify manually (Optional)**:\n"
|
|
108
|
-
" - After updating, run 'wsl lsmod | findstr binder' "
|
|
109
|
-
"in your terminal.\n"
|
|
110
|
-
" - If it shows 'binder_linux', "
|
|
111
|
-
"the issue should be resolved.\n"
|
|
112
|
-
"=================================================="
|
|
113
|
-
)
|
|
114
|
-
raise HostPrerequisiteError(error_message)
|
|
115
|
-
|
|
116
|
-
logger.info("Host environment check passed.")
|
|
117
|
-
|
|
118
|
-
|
|
119
18
|
class MobileMixin:
|
|
120
19
|
@property
|
|
121
20
|
def mobile_url(self):
|
|
@@ -142,41 +41,9 @@ class MobileMixin:
|
|
|
142
41
|
)
|
|
143
42
|
|
|
144
43
|
|
|
145
|
-
class
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Asynchronously retrieve the mobile VNC/websockify connection URL.
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
str: Fully qualified URL to access the mobile sandbox UI remotely.
|
|
152
|
-
|
|
153
|
-
Raises:
|
|
154
|
-
RuntimeError: If the sandbox is not healthy.
|
|
155
|
-
"""
|
|
156
|
-
# Check health asynchronously
|
|
157
|
-
is_healthy = await self.manager_api.check_health_async(
|
|
158
|
-
identity=self.sandbox_id,
|
|
159
|
-
)
|
|
160
|
-
if not is_healthy:
|
|
161
|
-
raise RuntimeError(f"Sandbox {self.sandbox_id} is not healthy")
|
|
162
|
-
|
|
163
|
-
# Get container info asynchronously
|
|
164
|
-
info = await self.get_info_async()
|
|
165
|
-
|
|
166
|
-
# Local path and remote path (currently the same)
|
|
167
|
-
path = "/websockify/"
|
|
168
|
-
remote_path = "/websockify/"
|
|
169
|
-
params = {"password": info["runtime_token"]}
|
|
170
|
-
|
|
171
|
-
# Local URL if base_url is not set
|
|
172
|
-
if self.base_url is None:
|
|
173
|
-
return urljoin(info["url"], path) + "?" + urlencode(params)
|
|
174
|
-
|
|
175
|
-
# Remote URL
|
|
176
|
-
return (
|
|
177
|
-
f"{self.base_url}/desktop/{self.sandbox_id}{remote_path}"
|
|
178
|
-
f"?{urlencode(params)}"
|
|
179
|
-
)
|
|
44
|
+
class HostPrerequisiteError(Exception):
|
|
45
|
+
"""Exception raised when host prerequisites
|
|
46
|
+
for MobileSandbox are not met."""
|
|
180
47
|
|
|
181
48
|
|
|
182
49
|
@SandboxRegistry.register(
|
|
@@ -199,7 +66,7 @@ class MobileSandbox(MobileMixin, Sandbox):
|
|
|
199
66
|
sandbox_type: SandboxType = SandboxType.MOBILE,
|
|
200
67
|
):
|
|
201
68
|
if base_url is None and not self.__class__._host_check_done:
|
|
202
|
-
_check_host_readiness()
|
|
69
|
+
self._check_host_readiness()
|
|
203
70
|
self.__class__._host_check_done = True
|
|
204
71
|
|
|
205
72
|
super().__init__(
|
|
@@ -210,6 +77,101 @@ class MobileSandbox(MobileMixin, Sandbox):
|
|
|
210
77
|
sandbox_type,
|
|
211
78
|
)
|
|
212
79
|
|
|
80
|
+
def _check_host_readiness(self) -> None:
|
|
81
|
+
logger.info(
|
|
82
|
+
"Performing one-time host environment check for MobileSandbox...",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
architecture = platform.machine().lower()
|
|
86
|
+
if architecture in ("aarch64", "arm64"):
|
|
87
|
+
logger.warning(
|
|
88
|
+
"\n======================== WARNING ========================\n"
|
|
89
|
+
"ARM64/aarch64 architecture detected (e.g., Apple M-series).\n"
|
|
90
|
+
"Running this mobile sandbox on a non-x86_64 host may lead \n"
|
|
91
|
+
" to unexpected compatibility or performance issues.\n"
|
|
92
|
+
"=========================================================",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
os_type = platform.system()
|
|
96
|
+
if os_type == "Linux":
|
|
97
|
+
try:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["lsmod"],
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
check=True,
|
|
103
|
+
)
|
|
104
|
+
loaded_modules = result.stdout
|
|
105
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
106
|
+
loaded_modules = ""
|
|
107
|
+
logger.warning(
|
|
108
|
+
"Could not execute 'lsmod' to verify kernel modules.",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if "binder_linux" not in loaded_modules:
|
|
112
|
+
error_message = (
|
|
113
|
+
"\n========== HOST PREREQUISITE FAILED ==========\n"
|
|
114
|
+
"MobileSandbox requires specific kernel modules"
|
|
115
|
+
" that appear to be missing or not loaded.\n\n"
|
|
116
|
+
"To fix this, please run the following commands"
|
|
117
|
+
" on your Linux host:\n\n"
|
|
118
|
+
"## Install required kernel modules\n"
|
|
119
|
+
"sudo apt update"
|
|
120
|
+
" && sudo apt install -y linux-modules-extra-`uname -r`\n"
|
|
121
|
+
"sudo modprobe binder_linux"
|
|
122
|
+
' devices="binder,hwbinder,vndbinder"\n'
|
|
123
|
+
"## (Optional) Load the ashmem driver for older kernels\n"
|
|
124
|
+
"sudo modprobe ashmem_linux\n"
|
|
125
|
+
"=================================================="
|
|
126
|
+
)
|
|
127
|
+
raise HostPrerequisiteError(error_message)
|
|
128
|
+
|
|
129
|
+
if os_type == "Windows":
|
|
130
|
+
try:
|
|
131
|
+
result = subprocess.run(
|
|
132
|
+
["wsl", "lsmod"],
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
check=True,
|
|
136
|
+
encoding="utf-8",
|
|
137
|
+
)
|
|
138
|
+
loaded_modules = result.stdout
|
|
139
|
+
except (FileNotFoundError, subprocess.CalledProcessError):
|
|
140
|
+
loaded_modules = ""
|
|
141
|
+
logger.warning(
|
|
142
|
+
"Could not execute 'wsl lsmod' to verify kernel modules.",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if "binder_linux" not in loaded_modules:
|
|
146
|
+
error_message = (
|
|
147
|
+
"\n========== HOST PREREQUISITE FAILED ==========\n"
|
|
148
|
+
"MobileSandbox on Windows requires Docker Desktop "
|
|
149
|
+
"with the WSL 2 backend.\n"
|
|
150
|
+
"The required kernel modules seem to be missing "
|
|
151
|
+
"in your WSL 2 environment.\n\n"
|
|
152
|
+
"To fix this, please follow these steps:\n\n"
|
|
153
|
+
"1. **Ensure Docker Desktop is using WSL 2**:\n"
|
|
154
|
+
" - Open Docker Desktop -> Settings -> General.\n"
|
|
155
|
+
" - Make sure 'Use the WSL 2 based engine' "
|
|
156
|
+
"is checked.\n\n"
|
|
157
|
+
"2. **Ensure WSL is installed and updated**:\n"
|
|
158
|
+
" - Open PowerShell or Command Prompt "
|
|
159
|
+
"as Administrator.\n"
|
|
160
|
+
" - Run: wsl --install\n"
|
|
161
|
+
" - Run: wsl --update\n"
|
|
162
|
+
" (An update usually installs a recent Linux kernel "
|
|
163
|
+
"with the required modules.)\n\n"
|
|
164
|
+
"3. **Verify manually (Optional)**:\n"
|
|
165
|
+
" - After updating, run 'wsl lsmod | findstr binder' "
|
|
166
|
+
"in your terminal.\n"
|
|
167
|
+
" - If it shows 'binder_linux', "
|
|
168
|
+
"the issue should be resolved.\n"
|
|
169
|
+
"=================================================="
|
|
170
|
+
)
|
|
171
|
+
raise HostPrerequisiteError(error_message)
|
|
172
|
+
|
|
173
|
+
logger.info("Host environment check passed.")
|
|
174
|
+
|
|
213
175
|
def adb_use(
|
|
214
176
|
self,
|
|
215
177
|
action: str,
|
|
@@ -326,112 +288,3 @@ class MobileSandbox(MobileMixin, Sandbox):
|
|
|
326
288
|
def mobile_get_screenshot(self):
|
|
327
289
|
"""Take a screenshot of the current device screen."""
|
|
328
290
|
return self.call_tool("adb", {"action": "get_screenshot"})
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
@SandboxRegistry.register(
|
|
332
|
-
build_image_uri("runtime-sandbox-mobile"),
|
|
333
|
-
sandbox_type=SandboxType.MOBILE_ASYNC,
|
|
334
|
-
security_level="high",
|
|
335
|
-
timeout=TIMEOUT,
|
|
336
|
-
description="Mobile Sandbox (Async)",
|
|
337
|
-
runtime_config={"privileged": True},
|
|
338
|
-
)
|
|
339
|
-
class MobileSandboxAsync(MobileMixin, AsyncMobileMixin, SandboxAsync):
|
|
340
|
-
_host_check_done = False
|
|
341
|
-
|
|
342
|
-
def __init__(
|
|
343
|
-
self,
|
|
344
|
-
sandbox_id: Optional[str] = None,
|
|
345
|
-
timeout: int = 3000,
|
|
346
|
-
base_url: Optional[str] = None,
|
|
347
|
-
bearer_token: Optional[str] = None,
|
|
348
|
-
sandbox_type: SandboxType = SandboxType.MOBILE_ASYNC,
|
|
349
|
-
):
|
|
350
|
-
if base_url is None and not self.__class__._host_check_done:
|
|
351
|
-
_check_host_readiness()
|
|
352
|
-
self.__class__._host_check_done = True
|
|
353
|
-
|
|
354
|
-
super().__init__(
|
|
355
|
-
sandbox_id,
|
|
356
|
-
timeout,
|
|
357
|
-
base_url,
|
|
358
|
-
bearer_token,
|
|
359
|
-
sandbox_type,
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
async def adb_use(
|
|
363
|
-
self,
|
|
364
|
-
action: str,
|
|
365
|
-
coordinate: Optional[List[int]] = None,
|
|
366
|
-
start: Optional[List[int]] = None,
|
|
367
|
-
end: Optional[List[int]] = None,
|
|
368
|
-
duration: Optional[int] = None,
|
|
369
|
-
code: Optional[Union[int, str]] = None,
|
|
370
|
-
text: Optional[str] = None,
|
|
371
|
-
):
|
|
372
|
-
"""
|
|
373
|
-
Asynchronously execute a general-purpose ADB action.
|
|
374
|
-
"""
|
|
375
|
-
payload = {"action": action}
|
|
376
|
-
if coordinate is not None:
|
|
377
|
-
payload["coordinate"] = coordinate
|
|
378
|
-
if start is not None:
|
|
379
|
-
payload["start"] = start
|
|
380
|
-
if end is not None:
|
|
381
|
-
payload["end"] = end
|
|
382
|
-
if duration is not None:
|
|
383
|
-
payload["duration"] = duration
|
|
384
|
-
if code is not None:
|
|
385
|
-
payload["code"] = code
|
|
386
|
-
if text is not None:
|
|
387
|
-
payload["text"] = text
|
|
388
|
-
|
|
389
|
-
return await self.call_tool_async("adb", payload)
|
|
390
|
-
|
|
391
|
-
async def mobile_get_screen_resolution(self):
|
|
392
|
-
"""Asynchronously get the screen resolution."""
|
|
393
|
-
return await self.call_tool_async(
|
|
394
|
-
"adb",
|
|
395
|
-
{"action": "get_screen_resolution"},
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
async def mobile_tap(self, coordinate: List[int]):
|
|
399
|
-
"""Asynchronously tap specific screen coordinates."""
|
|
400
|
-
return await self.call_tool_async(
|
|
401
|
-
"adb",
|
|
402
|
-
{"action": "tap", "coordinate": coordinate},
|
|
403
|
-
)
|
|
404
|
-
|
|
405
|
-
async def mobile_swipe(
|
|
406
|
-
self,
|
|
407
|
-
start: List[int],
|
|
408
|
-
end: List[int],
|
|
409
|
-
duration: Optional[int] = None,
|
|
410
|
-
):
|
|
411
|
-
"""Asynchronously perform a swipe gesture."""
|
|
412
|
-
payload = {
|
|
413
|
-
"action": "swipe",
|
|
414
|
-
"start": start,
|
|
415
|
-
"end": end,
|
|
416
|
-
}
|
|
417
|
-
if duration is not None:
|
|
418
|
-
payload["duration"] = duration
|
|
419
|
-
return await self.call_tool_async("adb", payload)
|
|
420
|
-
|
|
421
|
-
async def mobile_input_text(self, text: str):
|
|
422
|
-
"""Asynchronously input text into the focused UI element."""
|
|
423
|
-
return await self.call_tool_async(
|
|
424
|
-
"adb",
|
|
425
|
-
{"action": "input_text", "text": text},
|
|
426
|
-
)
|
|
427
|
-
|
|
428
|
-
async def mobile_key_event(self, code: Union[int, str]):
|
|
429
|
-
"""Asynchronously send a key event to the device."""
|
|
430
|
-
return await self.call_tool_async(
|
|
431
|
-
"adb",
|
|
432
|
-
{"action": "key_event", "code": code},
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
async def mobile_get_screenshot(self):
|
|
436
|
-
"""Asynchronously take a screenshot."""
|
|
437
|
-
return await self.call_tool_async("adb", {"action": "get_screenshot"})
|
|
@@ -12,46 +12,71 @@ logging.basicConfig(level=logging.INFO)
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
class
|
|
15
|
+
class Sandbox:
|
|
16
16
|
"""
|
|
17
|
-
|
|
17
|
+
Sandbox Interface.
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
def __init__(
|
|
21
21
|
self,
|
|
22
22
|
sandbox_id: Optional[str] = None,
|
|
23
|
-
timeout: int = 3000,
|
|
23
|
+
timeout: int = 3000, # TODO: enable life circle management
|
|
24
24
|
base_url: Optional[str] = None,
|
|
25
|
-
bearer_token: Optional[str] = None,
|
|
25
|
+
bearer_token: Optional[str] = None, # TODO: support api_key
|
|
26
26
|
sandbox_type: SandboxType = SandboxType.BASE,
|
|
27
27
|
) -> None:
|
|
28
|
+
"""
|
|
29
|
+
Initialize the sandbox interface.
|
|
30
|
+
"""
|
|
28
31
|
self.base_url = base_url
|
|
29
|
-
self.embed_mode = not bool(base_url)
|
|
30
|
-
self.sandbox_type = sandbox_type
|
|
31
|
-
self.timeout = timeout
|
|
32
|
-
self._sandbox_id = sandbox_id
|
|
33
|
-
|
|
34
32
|
if base_url:
|
|
33
|
+
self.embed_mode = False
|
|
35
34
|
self.manager_api = SandboxManager(
|
|
36
35
|
base_url=base_url,
|
|
37
36
|
bearer_token=bearer_token,
|
|
38
|
-
)
|
|
37
|
+
).__enter__()
|
|
39
38
|
else:
|
|
39
|
+
# Launch a local manager
|
|
40
|
+
self.embed_mode = True
|
|
40
41
|
self.manager_api = SandboxManager(
|
|
41
42
|
default_type=sandbox_type,
|
|
42
43
|
)
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
if sandbox_id is None:
|
|
46
|
+
logger.debug(
|
|
47
|
+
"You are using embed mode, it might take several seconds to "
|
|
48
|
+
"init the runtime, please wait.",
|
|
49
|
+
)
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
sandbox_id = self.manager_api.create_from_pool(
|
|
52
|
+
sandbox_type=SandboxType(sandbox_type).value,
|
|
53
|
+
)
|
|
54
|
+
if sandbox_id is None:
|
|
55
|
+
raise RuntimeError(
|
|
56
|
+
"No sandbox available. "
|
|
57
|
+
"Please check if sandbox images exist, build or pull "
|
|
58
|
+
"missing images in sandbox server.",
|
|
59
|
+
)
|
|
60
|
+
self._sandbox_id = sandbox_id
|
|
61
|
+
|
|
62
|
+
self._sandbox_id = sandbox_id
|
|
63
|
+
self.sandbox_type = sandbox_type
|
|
64
|
+
self.timeout = timeout
|
|
65
|
+
|
|
66
|
+
# Clean up function enabled in embed mode
|
|
67
|
+
if self.embed_mode:
|
|
68
|
+
atexit.register(self._cleanup)
|
|
69
|
+
self._register_signal_handlers()
|
|
70
|
+
|
|
71
|
+
def _register_signal_handlers(self) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Register signal handlers for graceful shutdown and cleanup.
|
|
74
|
+
Handles SIGINT (Ctrl+C) and, if available, SIGTERM to ensure that
|
|
75
|
+
the sandbox is properly cleaned up when the process receives these
|
|
76
|
+
signals. On platforms where SIGTERM is not available (e.g.,
|
|
77
|
+
Windows), only SIGINT is handled.
|
|
78
|
+
"""
|
|
53
79
|
|
|
54
|
-
def _register_signal_handlers(self):
|
|
55
80
|
def _handler(signum, frame): # pylint: disable=unused-argument
|
|
56
81
|
logger.debug(
|
|
57
82
|
f"Received signal {signum}, stopping Sandbox"
|
|
@@ -60,6 +85,7 @@ class SandboxBase:
|
|
|
60
85
|
self._cleanup()
|
|
61
86
|
raise SystemExit(0)
|
|
62
87
|
|
|
88
|
+
# Windows does not support SIGTERM
|
|
63
89
|
if hasattr(signal, "SIGTERM"):
|
|
64
90
|
signals = [signal.SIGINT, signal.SIGTERM]
|
|
65
91
|
else:
|
|
@@ -81,36 +107,39 @@ class SandboxBase:
|
|
|
81
107
|
specific sandbox instance.
|
|
82
108
|
"""
|
|
83
109
|
try:
|
|
110
|
+
# Remote not need to close the embed_manager
|
|
84
111
|
if self.embed_mode:
|
|
112
|
+
# Clean all
|
|
85
113
|
self.manager_api.__exit__(None, None, None)
|
|
86
114
|
else:
|
|
115
|
+
# Clean the specific sandbox
|
|
87
116
|
self.manager_api.release(self.sandbox_id)
|
|
88
117
|
except Exception as e:
|
|
89
118
|
import traceback
|
|
90
119
|
|
|
91
120
|
logger.error(
|
|
92
|
-
f"Cleanup {self.sandbox_id} error: {e}"
|
|
93
|
-
f"
|
|
121
|
+
f"Cleanup {self.sandbox_id} error: {e}\n"
|
|
122
|
+
f"{traceback.format_exc()}",
|
|
94
123
|
)
|
|
95
124
|
|
|
96
|
-
|
|
97
|
-
class Sandbox(SandboxBase):
|
|
98
125
|
def __enter__(self):
|
|
99
|
-
# Create sandbox if sandbox_id not provided
|
|
100
|
-
if self.sandbox_id is None:
|
|
101
|
-
self.sandbox_id = self.manager_api.create_from_pool(
|
|
102
|
-
sandbox_type=SandboxType(self.sandbox_type).value,
|
|
103
|
-
)
|
|
104
|
-
if self.sandbox_id is None:
|
|
105
|
-
raise RuntimeError("No sandbox available.")
|
|
106
|
-
if self.embed_mode:
|
|
107
|
-
atexit.register(self._cleanup)
|
|
108
|
-
self._register_signal_handlers()
|
|
109
126
|
return self
|
|
110
127
|
|
|
111
128
|
def __exit__(self, exc_type, exc_value, traceback):
|
|
112
129
|
self._cleanup()
|
|
113
130
|
|
|
131
|
+
@property
|
|
132
|
+
def sandbox_id(self) -> str:
|
|
133
|
+
"""Get the sandbox ID."""
|
|
134
|
+
return self._sandbox_id
|
|
135
|
+
|
|
136
|
+
@sandbox_id.setter
|
|
137
|
+
def sandbox_id(self, value: str) -> None:
|
|
138
|
+
"""Set the sandbox ID."""
|
|
139
|
+
if not value:
|
|
140
|
+
raise ValueError("Sandbox ID cannot be empty.")
|
|
141
|
+
self._sandbox_id = value
|
|
142
|
+
|
|
114
143
|
def get_info(self) -> dict:
|
|
115
144
|
return self.manager_api.get_info(self.sandbox_id)
|
|
116
145
|
|
|
@@ -127,77 +156,15 @@ class Sandbox(SandboxBase):
|
|
|
127
156
|
) -> Any:
|
|
128
157
|
if arguments is None:
|
|
129
158
|
arguments = {}
|
|
130
|
-
return self.manager_api.call_tool(self.sandbox_id, name, arguments)
|
|
131
159
|
|
|
132
|
-
|
|
133
|
-
return self.manager_api.add_mcp_servers(
|
|
134
|
-
self.sandbox_id,
|
|
135
|
-
server_configs,
|
|
136
|
-
overwrite,
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
class SandboxAsync(SandboxBase):
|
|
141
|
-
async def __aenter__(self):
|
|
142
|
-
if self.sandbox_id is None:
|
|
143
|
-
self.sandbox_id = await self.manager_api.create_from_pool_async(
|
|
144
|
-
sandbox_type=SandboxType(self.sandbox_type).value,
|
|
145
|
-
)
|
|
146
|
-
if self.sandbox_id is None:
|
|
147
|
-
raise RuntimeError("No sandbox available.")
|
|
148
|
-
if self.embed_mode:
|
|
149
|
-
atexit.register(self._cleanup)
|
|
150
|
-
self._register_signal_handlers()
|
|
151
|
-
return self
|
|
152
|
-
|
|
153
|
-
async def __aexit__(self, exc_type, exc_value, traceback):
|
|
154
|
-
await self._cleanup_async()
|
|
155
|
-
|
|
156
|
-
async def _cleanup_async(self):
|
|
157
|
-
try:
|
|
158
|
-
if self.embed_mode:
|
|
159
|
-
await self.manager_api.__aexit__(None, None, None)
|
|
160
|
-
else:
|
|
161
|
-
await self.manager_api.release_async(self.sandbox_id)
|
|
162
|
-
except Exception as e:
|
|
163
|
-
import traceback
|
|
164
|
-
|
|
165
|
-
logger.error(
|
|
166
|
-
f"Async Cleanup {self.sandbox_id} error: {e}"
|
|
167
|
-
f"\n{traceback.format_exc()}",
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
async def get_info_async(self) -> dict:
|
|
171
|
-
return await self.manager_api.get_info_async(self.sandbox_id)
|
|
172
|
-
|
|
173
|
-
async def list_tools_async(
|
|
174
|
-
self,
|
|
175
|
-
tool_type: Optional[str] = None,
|
|
176
|
-
) -> dict:
|
|
177
|
-
return await self.manager_api.list_tools_async(
|
|
178
|
-
self.sandbox_id,
|
|
179
|
-
tool_type=tool_type,
|
|
180
|
-
)
|
|
181
|
-
|
|
182
|
-
async def call_tool_async(
|
|
183
|
-
self,
|
|
184
|
-
name: str,
|
|
185
|
-
arguments: Optional[dict[str, Any]] = None,
|
|
186
|
-
) -> Any:
|
|
187
|
-
if arguments is None:
|
|
188
|
-
arguments = {}
|
|
189
|
-
return await self.manager_api.call_tool_async(
|
|
190
|
-
self.sandbox_id,
|
|
191
|
-
name,
|
|
192
|
-
arguments,
|
|
193
|
-
)
|
|
160
|
+
return self.manager_api.call_tool(self.sandbox_id, name, arguments)
|
|
194
161
|
|
|
195
|
-
|
|
162
|
+
def add_mcp_servers(
|
|
196
163
|
self,
|
|
197
164
|
server_configs: dict,
|
|
198
165
|
overwrite=False,
|
|
199
166
|
):
|
|
200
|
-
return
|
|
167
|
+
return self.manager_api.add_mcp_servers(
|
|
201
168
|
self.sandbox_id,
|
|
202
169
|
server_configs,
|
|
203
170
|
overwrite,
|