agentscope-runtime 1.0.4a1__py3-none-any.whl → 1.0.5.post1__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 +2 -8
- agentscope_runtime/adapters/langgraph/stream.py +120 -70
- agentscope_runtime/adapters/ms_agent_framework/__init__.py +0 -0
- agentscope_runtime/adapters/ms_agent_framework/message.py +205 -0
- agentscope_runtime/adapters/ms_agent_framework/stream.py +418 -0
- agentscope_runtime/adapters/utils.py +6 -0
- agentscope_runtime/cli/commands/deploy.py +836 -1
- agentscope_runtime/cli/commands/stop.py +16 -0
- agentscope_runtime/common/container_clients/__init__.py +52 -0
- agentscope_runtime/common/container_clients/agentrun_client.py +6 -4
- agentscope_runtime/common/container_clients/boxlite_client.py +442 -0
- agentscope_runtime/common/container_clients/docker_client.py +0 -20
- agentscope_runtime/common/container_clients/fc_client.py +6 -4
- agentscope_runtime/common/container_clients/gvisor_client.py +38 -0
- agentscope_runtime/common/container_clients/knative_client.py +467 -0
- agentscope_runtime/common/utils/deprecation.py +164 -0
- agentscope_runtime/engine/__init__.py +4 -0
- agentscope_runtime/engine/app/agent_app.py +16 -4
- agentscope_runtime/engine/constant.py +1 -0
- agentscope_runtime/engine/deployers/__init__.py +34 -11
- agentscope_runtime/engine/deployers/adapter/__init__.py +8 -0
- agentscope_runtime/engine/deployers/adapter/a2a/__init__.py +26 -51
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_protocol_adapter.py +23 -13
- agentscope_runtime/engine/deployers/adapter/a2a/a2a_registry.py +4 -201
- agentscope_runtime/engine/deployers/adapter/a2a/nacos_a2a_registry.py +152 -25
- agentscope_runtime/engine/deployers/adapter/agui/__init__.py +8 -0
- agentscope_runtime/engine/deployers/adapter/agui/agui_adapter_utils.py +652 -0
- agentscope_runtime/engine/deployers/adapter/agui/agui_protocol_adapter.py +225 -0
- agentscope_runtime/engine/deployers/agentrun_deployer.py +2 -2
- agentscope_runtime/engine/deployers/fc_deployer.py +1506 -0
- agentscope_runtime/engine/deployers/knative_deployer.py +290 -0
- agentscope_runtime/engine/deployers/pai_deployer.py +2335 -0
- agentscope_runtime/engine/deployers/utils/net_utils.py +37 -0
- agentscope_runtime/engine/deployers/utils/oss_utils.py +38 -0
- agentscope_runtime/engine/deployers/utils/package.py +46 -42
- agentscope_runtime/engine/helpers/agent_api_client.py +372 -0
- agentscope_runtime/engine/runner.py +13 -0
- agentscope_runtime/engine/schemas/agent_schemas.py +9 -3
- agentscope_runtime/engine/services/agent_state/__init__.py +7 -0
- agentscope_runtime/engine/services/memory/__init__.py +7 -0
- agentscope_runtime/engine/services/memory/redis_memory_service.py +15 -16
- agentscope_runtime/engine/services/session_history/__init__.py +7 -0
- agentscope_runtime/engine/tracing/local_logging_handler.py +2 -3
- agentscope_runtime/engine/tracing/wrapper.py +18 -4
- agentscope_runtime/sandbox/__init__.py +14 -6
- agentscope_runtime/sandbox/box/base/__init__.py +2 -2
- agentscope_runtime/sandbox/box/base/base_sandbox.py +51 -1
- agentscope_runtime/sandbox/box/browser/__init__.py +2 -2
- agentscope_runtime/sandbox/box/browser/browser_sandbox.py +198 -2
- agentscope_runtime/sandbox/box/filesystem/__init__.py +2 -2
- agentscope_runtime/sandbox/box/filesystem/filesystem_sandbox.py +99 -2
- agentscope_runtime/sandbox/box/gui/__init__.py +2 -2
- agentscope_runtime/sandbox/box/gui/gui_sandbox.py +117 -1
- agentscope_runtime/sandbox/box/mobile/__init__.py +2 -2
- agentscope_runtime/sandbox/box/mobile/mobile_sandbox.py +247 -100
- agentscope_runtime/sandbox/box/sandbox.py +102 -65
- agentscope_runtime/sandbox/box/shared/routers/generic.py +36 -29
- agentscope_runtime/sandbox/client/__init__.py +6 -1
- agentscope_runtime/sandbox/client/async_http_client.py +339 -0
- agentscope_runtime/sandbox/client/base.py +74 -0
- agentscope_runtime/sandbox/client/http_client.py +108 -329
- agentscope_runtime/sandbox/enums.py +7 -0
- agentscope_runtime/sandbox/manager/sandbox_manager.py +275 -29
- agentscope_runtime/sandbox/manager/server/app.py +7 -1
- agentscope_runtime/sandbox/manager/server/config.py +3 -1
- agentscope_runtime/sandbox/model/manager_config.py +11 -9
- agentscope_runtime/tools/modelstudio_memory/__init__.py +106 -0
- agentscope_runtime/tools/modelstudio_memory/base.py +220 -0
- agentscope_runtime/tools/modelstudio_memory/config.py +86 -0
- agentscope_runtime/tools/modelstudio_memory/core.py +594 -0
- agentscope_runtime/tools/modelstudio_memory/exceptions.py +60 -0
- agentscope_runtime/tools/modelstudio_memory/schemas.py +253 -0
- agentscope_runtime/version.py +1 -1
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/METADATA +187 -74
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/RECORD +79 -55
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/WHEEL +1 -1
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/entry_points.txt +0 -0
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/licenses/LICENSE +0 -0
- {agentscope_runtime-1.0.4a1.dist-info → agentscope_runtime-1.0.5.post1.dist-info}/top_level.txt +0 -0
|
@@ -8,7 +8,7 @@ from urllib.parse import urljoin, urlencode
|
|
|
8
8
|
from ...utils import build_image_uri, get_platform
|
|
9
9
|
from ...registry import SandboxRegistry
|
|
10
10
|
from ...enums import SandboxType
|
|
11
|
-
from ...box.base import BaseSandbox
|
|
11
|
+
from ...box.base import BaseSandbox, BaseSandboxAsync
|
|
12
12
|
from ...constant import TIMEOUT
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger(__name__)
|
|
@@ -34,6 +34,34 @@ class GUIMixin:
|
|
|
34
34
|
)
|
|
35
35
|
|
|
36
36
|
|
|
37
|
+
class AsyncGUIMixin:
|
|
38
|
+
async def get_desktop_url_async(self):
|
|
39
|
+
# Check sandbox health asynchronously
|
|
40
|
+
is_healthy = await self.manager_api.check_health_async(
|
|
41
|
+
identity=self.sandbox_id,
|
|
42
|
+
)
|
|
43
|
+
if not is_healthy:
|
|
44
|
+
raise RuntimeError(f"Sandbox {self.sandbox_id} is not healthy")
|
|
45
|
+
|
|
46
|
+
# Retrieve container information asynchronously
|
|
47
|
+
info = await self.get_info_async()
|
|
48
|
+
|
|
49
|
+
# Default local VNC path and remote VNC relay path
|
|
50
|
+
path = "/vnc/vnc_lite.html"
|
|
51
|
+
remote_path = "/vnc/vnc_relay.html"
|
|
52
|
+
params = {"password": info["runtime_token"]}
|
|
53
|
+
|
|
54
|
+
# If base_url is not set, construct the local URL
|
|
55
|
+
if self.base_url is None:
|
|
56
|
+
return urljoin(info["url"], path) + "?" + urlencode(params)
|
|
57
|
+
|
|
58
|
+
# Construct the remote URL with sandbox ID and VNC relay path
|
|
59
|
+
return (
|
|
60
|
+
f"{self.base_url}/desktop/{self.sandbox_id}{remote_path}"
|
|
61
|
+
f"?{urlencode(params)}"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
37
65
|
@SandboxRegistry.register(
|
|
38
66
|
build_image_uri("runtime-sandbox-gui"),
|
|
39
67
|
sandbox_type=SandboxType.GUI,
|
|
@@ -121,3 +149,91 @@ class GuiSandbox(GUIMixin, BaseSandbox):
|
|
|
121
149
|
payload["text"] = text
|
|
122
150
|
|
|
123
151
|
return self.call_tool("computer", payload)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@SandboxRegistry.register(
|
|
155
|
+
build_image_uri("runtime-sandbox-gui"),
|
|
156
|
+
sandbox_type=SandboxType.GUI_ASYNC,
|
|
157
|
+
security_level="high",
|
|
158
|
+
timeout=TIMEOUT,
|
|
159
|
+
description="GUI Sandbox (Async)",
|
|
160
|
+
)
|
|
161
|
+
class GuiSandboxAsync(GUIMixin, AsyncGUIMixin, BaseSandboxAsync):
|
|
162
|
+
def __init__( # pylint: disable=useless-parent-delegation
|
|
163
|
+
self,
|
|
164
|
+
sandbox_id: Optional[str] = None,
|
|
165
|
+
timeout: int = 3000,
|
|
166
|
+
base_url: Optional[str] = None,
|
|
167
|
+
bearer_token: Optional[str] = None,
|
|
168
|
+
sandbox_type: SandboxType = SandboxType.GUI_ASYNC,
|
|
169
|
+
):
|
|
170
|
+
super().__init__(
|
|
171
|
+
sandbox_id,
|
|
172
|
+
timeout,
|
|
173
|
+
base_url,
|
|
174
|
+
bearer_token,
|
|
175
|
+
sandbox_type,
|
|
176
|
+
)
|
|
177
|
+
# Architecture compatibility warning
|
|
178
|
+
if get_platform() == "linux/arm64":
|
|
179
|
+
logger.warning(
|
|
180
|
+
"\nCompatibility Notice: This GUI Sandbox may have issues on "
|
|
181
|
+
"arm64 CPU architectures, due to the computer-use-mcp not "
|
|
182
|
+
"providing linux/arm64 compatibility. It has been tested "
|
|
183
|
+
"on Apple M4 chips with Rosetta enabled. However, on M1, M2, "
|
|
184
|
+
"and M3 chips, Chromium browser might crash due to the missing "
|
|
185
|
+
"SSE3 instruction set.",
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
async def computer_use(
|
|
189
|
+
self,
|
|
190
|
+
action: str,
|
|
191
|
+
coordinate: Optional[Union[List[float], Tuple[float, float]]] = None,
|
|
192
|
+
text: Optional[str] = None,
|
|
193
|
+
):
|
|
194
|
+
"""
|
|
195
|
+
Asynchronously use mouse and keyboard to interact with a desktop GUI.
|
|
196
|
+
|
|
197
|
+
This method interfaces with the sandbox's GUI environment.
|
|
198
|
+
You do not have access to a terminal or applications menu;
|
|
199
|
+
interaction is performed by clicking on desktop icons or using
|
|
200
|
+
keyboard shortcuts.
|
|
201
|
+
|
|
202
|
+
Guidelines:
|
|
203
|
+
* Prefer keyboard shortcuts where possible over cursor actions.
|
|
204
|
+
* If visual keyboard hints (two-letter boxes) are shown, typing
|
|
205
|
+
those letters will click the element — use this where possible.
|
|
206
|
+
* Applications or actions may require waiting; e.g., repeat
|
|
207
|
+
screenshots if windows don’t open immediately.
|
|
208
|
+
* Always determine cursor coordinates using screenshots before moving
|
|
209
|
+
the cursor to click on elements.
|
|
210
|
+
* If clicks fail to load content, try adjusting cursor coordinates to
|
|
211
|
+
center on the target element.
|
|
212
|
+
* Click with the cursor tip centered on elements, not on edges.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
action (str): The action to perform. Options include:
|
|
216
|
+
* `key` — Press a key or key combination.
|
|
217
|
+
* `type` — Type a string of text.
|
|
218
|
+
* `get_cursor_position` — Get current cursor coordinates (x, y).
|
|
219
|
+
* `mouse_move` — Move cursor to given (x, y).
|
|
220
|
+
* `left_click` — Left mouse click.
|
|
221
|
+
* `left_click_drag` — Click and drag to given (x, y).
|
|
222
|
+
* `right_click` — Right mouse click.
|
|
223
|
+
* `middle_click` — Middle mouse click.
|
|
224
|
+
* `double_click` — Double left mouse click.
|
|
225
|
+
* `get_screenshot` — Capture screen screenshot.
|
|
226
|
+
coordinate (list[float] | tuple[float, float], optional):
|
|
227
|
+
Pixel coordinates (x from left edge, y from top edge).
|
|
228
|
+
text (str, optional): String to type, or key-combination for `key` action.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Any: Tool execution result from the sandbox.
|
|
232
|
+
"""
|
|
233
|
+
payload = {"action": action}
|
|
234
|
+
if coordinate is not None:
|
|
235
|
+
payload["coordinate"] = coordinate
|
|
236
|
+
if text is not None:
|
|
237
|
+
payload["text"] = text
|
|
238
|
+
|
|
239
|
+
return await self.call_tool_async("computer", payload)
|
|
@@ -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 ..sandbox import Sandbox, SandboxAsync
|
|
9
9
|
|
|
10
10
|
from ...utils import build_image_uri
|
|
11
11
|
from ...registry import SandboxRegistry
|
|
@@ -15,6 +15,107 @@ 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
|
+
|
|
18
119
|
class MobileMixin:
|
|
19
120
|
@property
|
|
20
121
|
def mobile_url(self):
|
|
@@ -41,9 +142,41 @@ class MobileMixin:
|
|
|
41
142
|
)
|
|
42
143
|
|
|
43
144
|
|
|
44
|
-
class
|
|
45
|
-
|
|
46
|
-
|
|
145
|
+
class AsyncMobileMixin:
|
|
146
|
+
async def get_mobile_url_async(self):
|
|
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
|
+
)
|
|
47
180
|
|
|
48
181
|
|
|
49
182
|
@SandboxRegistry.register(
|
|
@@ -66,7 +199,7 @@ class MobileSandbox(MobileMixin, Sandbox):
|
|
|
66
199
|
sandbox_type: SandboxType = SandboxType.MOBILE,
|
|
67
200
|
):
|
|
68
201
|
if base_url is None and not self.__class__._host_check_done:
|
|
69
|
-
|
|
202
|
+
_check_host_readiness()
|
|
70
203
|
self.__class__._host_check_done = True
|
|
71
204
|
|
|
72
205
|
super().__init__(
|
|
@@ -77,101 +210,6 @@ class MobileSandbox(MobileMixin, Sandbox):
|
|
|
77
210
|
sandbox_type,
|
|
78
211
|
)
|
|
79
212
|
|
|
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
|
-
|
|
175
213
|
def adb_use(
|
|
176
214
|
self,
|
|
177
215
|
action: str,
|
|
@@ -288,3 +326,112 @@ class MobileSandbox(MobileMixin, Sandbox):
|
|
|
288
326
|
def mobile_get_screenshot(self):
|
|
289
327
|
"""Take a screenshot of the current device screen."""
|
|
290
328
|
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"})
|