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
|
@@ -61,6 +61,22 @@ def _create_deployer(
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
return AgentRunDeployManager()
|
|
64
|
+
|
|
65
|
+
elif platform == "pai":
|
|
66
|
+
from agentscope_runtime.engine.deployers.pai_deployer import (
|
|
67
|
+
PAIDeployManager,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Extract workspace_id from deployment config
|
|
71
|
+
config = deployment_state.get("config", {})
|
|
72
|
+
workspace_id = config.get("workspace_id")
|
|
73
|
+
region = config.get("region_id")
|
|
74
|
+
oss_path = config.get("oss_path")
|
|
75
|
+
return PAIDeployManager(
|
|
76
|
+
workspace_id=workspace_id,
|
|
77
|
+
region_id=region,
|
|
78
|
+
oss_path=oss_path,
|
|
79
|
+
)
|
|
64
80
|
else:
|
|
65
81
|
echo_warning(f"Unknown platform: {platform}")
|
|
66
82
|
return None
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import sys
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ..utils.lazy_loader import install_lazy_loader
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .docker_client import DockerClient
|
|
9
|
+
from .kubernetes_client import KubernetesClient
|
|
10
|
+
from .knative_client import KnativeClient
|
|
11
|
+
from .fc_client import FCClient
|
|
12
|
+
from .agentrun_client import AgentRunClient
|
|
13
|
+
from .gvisor_client import GVisorDockerClient
|
|
14
|
+
from .boxlite_client import BoxliteClient
|
|
15
|
+
|
|
16
|
+
install_lazy_loader(
|
|
17
|
+
globals(),
|
|
18
|
+
{
|
|
19
|
+
"DockerClient": ".docker_client",
|
|
20
|
+
"KubernetesClient": ".kubernetes_client",
|
|
21
|
+
"KnativeClient": ".knative_client",
|
|
22
|
+
"FCClient": ".fc_client",
|
|
23
|
+
"AgentRunClient": ".agentrun_client",
|
|
24
|
+
"GVisorDockerClient": ".gvisor_client",
|
|
25
|
+
"BoxliteClient": ".boxlite_client",
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ContainerClientFactory:
|
|
31
|
+
_CLIENT_MAPPING = {
|
|
32
|
+
"docker": "DockerClient",
|
|
33
|
+
"k8s": "KubernetesClient",
|
|
34
|
+
"knative": "KnativeClient",
|
|
35
|
+
"fc": "FCClient",
|
|
36
|
+
"agentrun": "AgentRunClient",
|
|
37
|
+
"gvisor": "GVisorDockerClient",
|
|
38
|
+
"boxlite": "BoxliteClient",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def create_client(cls, deployment_type, config):
|
|
43
|
+
try:
|
|
44
|
+
class_name = cls._CLIENT_MAPPING[deployment_type]
|
|
45
|
+
except KeyError as e:
|
|
46
|
+
raise NotImplementedError(
|
|
47
|
+
f"Container deployment '{deployment_type}' not implemented",
|
|
48
|
+
) from e
|
|
49
|
+
|
|
50
|
+
module = sys.modules[__name__]
|
|
51
|
+
client_class = getattr(module, class_name)
|
|
52
|
+
return client_class(config=config)
|
|
@@ -1078,13 +1078,15 @@ class AgentRunClient(BaseClient):
|
|
|
1078
1078
|
"""
|
|
1079
1079
|
replacement_map = {
|
|
1080
1080
|
"agentscope/runtime-sandbox-base": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
1081
|
-
"/agentscope_runtime-sandbox-base:
|
|
1081
|
+
"/agentscope_runtime-sandbox-base:20260106",
|
|
1082
1082
|
"agentscope/runtime-sandbox-browser": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
1083
|
-
"/agentscope_runtime-sandbox-browser:
|
|
1083
|
+
"/agentscope_runtime-sandbox-browser:20260106",
|
|
1084
1084
|
"agentscope/runtime-sandbox-filesystem": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
1085
|
-
"/agentscope_runtime-sandbox-filesystem:
|
|
1085
|
+
"/agentscope_runtime-sandbox-filesystem:20260106",
|
|
1086
1086
|
"agentscope/runtime-sandbox-gui": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
1087
|
-
"/agentscope_runtime-sandbox-gui:
|
|
1087
|
+
"/agentscope_runtime-sandbox-gui:20260106",
|
|
1088
|
+
"agentscope/runtime-sandbox-mobile": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
1089
|
+
"/agentscope_runtime-sandbox-mobile:20251217",
|
|
1088
1090
|
}
|
|
1089
1091
|
|
|
1090
1092
|
if ":" in image:
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# pylint: disable=too-many-branches
|
|
3
|
+
import atexit
|
|
4
|
+
import logging
|
|
5
|
+
import socket
|
|
6
|
+
import traceback
|
|
7
|
+
|
|
8
|
+
import boxlite
|
|
9
|
+
from boxlite import SyncBoxlite
|
|
10
|
+
|
|
11
|
+
from .base_client import BaseClient
|
|
12
|
+
from ..collections import (
|
|
13
|
+
RedisSetCollection,
|
|
14
|
+
InMemorySetCollection,
|
|
15
|
+
RedisMapping,
|
|
16
|
+
InMemoryMapping,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BoxliteClient(BaseClient):
|
|
23
|
+
"""
|
|
24
|
+
BoxLite client implementation that provides a Docker-like interface
|
|
25
|
+
for managing boxes using the BoxLite SDK.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self, config=None):
|
|
29
|
+
"""
|
|
30
|
+
Initialize the BoxLite client.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
config: Configuration object with optional attributes:
|
|
34
|
+
- port_range: Tuple of (start, end) for port range
|
|
35
|
+
(default: (8000, 9000))
|
|
36
|
+
- redis_enabled: Whether to use Redis for port management
|
|
37
|
+
(default: False)
|
|
38
|
+
- redis_server: Redis server host (default: 'localhost')
|
|
39
|
+
- redis_port: Redis server port (default: 6379)
|
|
40
|
+
- redis_db: Redis database number (default: 0)
|
|
41
|
+
- redis_user: Redis username (optional)
|
|
42
|
+
- redis_password: Redis password (optional)
|
|
43
|
+
- redis_port_key: Redis key prefix for ports
|
|
44
|
+
(default: 'boxlite:ports')
|
|
45
|
+
"""
|
|
46
|
+
self.config = config
|
|
47
|
+
|
|
48
|
+
self.port_range = range(*self.config.port_range)
|
|
49
|
+
|
|
50
|
+
# Initialize port management
|
|
51
|
+
if hasattr(self.config, "redis_enabled") and self.config.redis_enabled:
|
|
52
|
+
import redis
|
|
53
|
+
|
|
54
|
+
redis_client = redis.Redis(
|
|
55
|
+
host=getattr(self.config, "redis_server", "localhost"),
|
|
56
|
+
port=getattr(self.config, "redis_port", 6379),
|
|
57
|
+
db=getattr(self.config, "redis_db", 0),
|
|
58
|
+
username=getattr(self.config, "redis_user", None),
|
|
59
|
+
password=getattr(self.config, "redis_password", None),
|
|
60
|
+
decode_responses=True,
|
|
61
|
+
)
|
|
62
|
+
try:
|
|
63
|
+
redis_client.ping()
|
|
64
|
+
except ConnectionError as e:
|
|
65
|
+
raise RuntimeError(
|
|
66
|
+
"Unable to connect to the Redis server.",
|
|
67
|
+
) from e
|
|
68
|
+
|
|
69
|
+
self.port_set = RedisSetCollection(
|
|
70
|
+
redis_client,
|
|
71
|
+
set_name=getattr(
|
|
72
|
+
self.config,
|
|
73
|
+
"redis_port_key",
|
|
74
|
+
"boxlite:ports",
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
self.ports_cache = RedisMapping(
|
|
78
|
+
redis_client,
|
|
79
|
+
prefix=getattr(self.config, "redis_port_key", "boxlite:ports"),
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
# Use in-memory collections
|
|
83
|
+
self.port_set = InMemorySetCollection()
|
|
84
|
+
self.ports_cache = InMemoryMapping()
|
|
85
|
+
|
|
86
|
+
# Initialize BoxLite runtime
|
|
87
|
+
try:
|
|
88
|
+
from ...sandbox.constant import REGISTRY
|
|
89
|
+
|
|
90
|
+
if REGISTRY:
|
|
91
|
+
image_registries = [REGISTRY]
|
|
92
|
+
else:
|
|
93
|
+
image_registries = ["ghcr.io", "docker.io"]
|
|
94
|
+
|
|
95
|
+
options = boxlite.Options(
|
|
96
|
+
image_registries=image_registries,
|
|
97
|
+
)
|
|
98
|
+
self.runtime = SyncBoxlite(options=options)
|
|
99
|
+
self.runtime.start()
|
|
100
|
+
except Exception as e:
|
|
101
|
+
raise RuntimeError(
|
|
102
|
+
f"BoxLite client initialization failed: {str(e)}\n"
|
|
103
|
+
"Solutions:\n"
|
|
104
|
+
"• Ensure BoxLite is properly installed\n"
|
|
105
|
+
"• Check BoxLite runtime configuration",
|
|
106
|
+
) from e
|
|
107
|
+
|
|
108
|
+
atexit.register(self._cleanup_runtime)
|
|
109
|
+
|
|
110
|
+
def _cleanup_runtime(self):
|
|
111
|
+
try:
|
|
112
|
+
if hasattr(self, "runtime") and self.runtime:
|
|
113
|
+
if hasattr(self.runtime, "__exit__"):
|
|
114
|
+
self.runtime.__exit__(None, None, None)
|
|
115
|
+
elif hasattr(self.runtime, "close"):
|
|
116
|
+
self.runtime.stop()
|
|
117
|
+
logger.info("BoxLite runtime cleaned up via atexit.")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
logger.warning(f"An error occurred during BoxLite cleanup: {e}")
|
|
120
|
+
logger.debug(traceback.format_exc())
|
|
121
|
+
|
|
122
|
+
def create(
|
|
123
|
+
self,
|
|
124
|
+
image,
|
|
125
|
+
name=None,
|
|
126
|
+
ports=None,
|
|
127
|
+
volumes=None,
|
|
128
|
+
environment=None,
|
|
129
|
+
runtime_config=None,
|
|
130
|
+
):
|
|
131
|
+
"""
|
|
132
|
+
Create a new BoxLite box.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
image: Container image to use
|
|
136
|
+
name: Optional name for the box
|
|
137
|
+
ports: List of container ports to expose (e.g., [8080, 3000])
|
|
138
|
+
volumes: List of volume mounts as
|
|
139
|
+
(host_path, guest_path, mode) tuples
|
|
140
|
+
environment: Dict of environment variables
|
|
141
|
+
runtime_config: Additional runtime configuration
|
|
142
|
+
(cpus, memory_mib, etc.)
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Tuple of (container_id, host_ports, host) or (None, None,
|
|
146
|
+
None) on failure
|
|
147
|
+
"""
|
|
148
|
+
if runtime_config is None:
|
|
149
|
+
runtime_config = {}
|
|
150
|
+
|
|
151
|
+
port_mapping = {}
|
|
152
|
+
|
|
153
|
+
if ports:
|
|
154
|
+
free_ports = self._find_free_ports(len(ports))
|
|
155
|
+
for container_port, host_port in zip(ports, free_ports):
|
|
156
|
+
port_mapping[container_port] = host_port
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
# Convert environment dict to list of tuples
|
|
160
|
+
env_list = []
|
|
161
|
+
if environment:
|
|
162
|
+
env_list = list(environment.items())
|
|
163
|
+
|
|
164
|
+
# Convert volumes to BoxLite format
|
|
165
|
+
volume_list = []
|
|
166
|
+
if volumes:
|
|
167
|
+
for vol in volumes:
|
|
168
|
+
if isinstance(vol, (list, tuple)) and len(vol) >= 2:
|
|
169
|
+
host_path = vol[0]
|
|
170
|
+
guest_path = vol[1]
|
|
171
|
+
read_only = len(vol) > 2 and vol[2] in (
|
|
172
|
+
"ro",
|
|
173
|
+
"readonly",
|
|
174
|
+
True,
|
|
175
|
+
)
|
|
176
|
+
volume_list.append(
|
|
177
|
+
(
|
|
178
|
+
host_path,
|
|
179
|
+
guest_path,
|
|
180
|
+
"ro" if read_only else "rw",
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Convert ports to BoxLite format
|
|
185
|
+
port_list = []
|
|
186
|
+
for container_port, host_port in port_mapping.items():
|
|
187
|
+
if isinstance(container_port, str):
|
|
188
|
+
if "/" in container_port:
|
|
189
|
+
container_port = container_port.split("/")[0]
|
|
190
|
+
port_list.append((int(host_port), int(container_port), "tcp"))
|
|
191
|
+
|
|
192
|
+
# Create BoxOptions
|
|
193
|
+
box_options = boxlite.BoxOptions(
|
|
194
|
+
image=image,
|
|
195
|
+
env=env_list,
|
|
196
|
+
volumes=volume_list,
|
|
197
|
+
ports=port_list,
|
|
198
|
+
auto_remove=False, # We'll manage removal ourselves
|
|
199
|
+
detach=True,
|
|
200
|
+
**runtime_config,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
# Create the box
|
|
204
|
+
box = self.runtime.create(box_options, name=name)
|
|
205
|
+
box_id = box.id
|
|
206
|
+
|
|
207
|
+
logger.debug(f"✓ Box created: {box.id}")
|
|
208
|
+
|
|
209
|
+
# Execute command (mirrors: await box.exec())
|
|
210
|
+
execution = box.exec("echo", ["Hello from default runtime"])
|
|
211
|
+
stdout = execution.stdout()
|
|
212
|
+
|
|
213
|
+
logger.debug("Output:")
|
|
214
|
+
for line in stdout: # Regular for loop, not async for
|
|
215
|
+
logger.debug(f" {line.strip()}")
|
|
216
|
+
|
|
217
|
+
exec_result = execution.wait() # No await
|
|
218
|
+
logger.debug(f"✓ Exit code: {exec_result.exit_code}")
|
|
219
|
+
|
|
220
|
+
# Store port mapping
|
|
221
|
+
if port_mapping:
|
|
222
|
+
self.ports_cache.set(box_id, list(port_mapping.values()))
|
|
223
|
+
|
|
224
|
+
return box_id, list(port_mapping.values()), "localhost"
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.warning(f"An error occurred: {e}")
|
|
227
|
+
logger.debug(f"{traceback.format_exc()}")
|
|
228
|
+
return None, None, None
|
|
229
|
+
|
|
230
|
+
def start(self, container_id):
|
|
231
|
+
"""
|
|
232
|
+
Start a BoxLite box.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
container_id: Box ID or name
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
bool: True if successful, False otherwise
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
box = self.runtime.get(container_id)
|
|
242
|
+
if box is None:
|
|
243
|
+
logger.warning(f"Box '{container_id}' not found")
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
box.start()
|
|
247
|
+
return True
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.warning(f"An error occurred: {e}")
|
|
250
|
+
logger.debug(f"{traceback.format_exc()}")
|
|
251
|
+
return False
|
|
252
|
+
|
|
253
|
+
def stop(self, container_id, timeout=None):
|
|
254
|
+
"""
|
|
255
|
+
Stop a BoxLite box.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
container_id: Box ID or name
|
|
259
|
+
timeout: Optional timeout in seconds (not used in BoxLite)
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
bool: True if successful, False otherwise
|
|
263
|
+
"""
|
|
264
|
+
try:
|
|
265
|
+
box = self.runtime.get(container_id)
|
|
266
|
+
if box is None:
|
|
267
|
+
logger.warning(f"Box '{container_id}' not found")
|
|
268
|
+
return False
|
|
269
|
+
|
|
270
|
+
# Stop the box
|
|
271
|
+
box.stop()
|
|
272
|
+
return True
|
|
273
|
+
except Exception as e:
|
|
274
|
+
logger.warning(f"An error occurred: {e}")
|
|
275
|
+
logger.debug(f"{traceback.format_exc()}")
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
def remove(self, container_id, force=False):
|
|
279
|
+
"""
|
|
280
|
+
Remove a BoxLite box.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
container_id: Box ID or name
|
|
284
|
+
force: If True, stop the box first if running
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
bool: True if successful, False otherwise
|
|
288
|
+
"""
|
|
289
|
+
try:
|
|
290
|
+
# Get ports before removal
|
|
291
|
+
ports = self.ports_cache.get(container_id)
|
|
292
|
+
|
|
293
|
+
# Remove the box
|
|
294
|
+
self.runtime.remove(container_id, force=force)
|
|
295
|
+
|
|
296
|
+
# Clean up port cache
|
|
297
|
+
self.ports_cache.delete(container_id)
|
|
298
|
+
|
|
299
|
+
# Remove ports from port set
|
|
300
|
+
if ports:
|
|
301
|
+
for host_port in ports:
|
|
302
|
+
self.port_set.remove(host_port)
|
|
303
|
+
|
|
304
|
+
return True
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.warning(f"An error occurred: {e}")
|
|
307
|
+
logger.debug(f"{traceback.format_exc()}")
|
|
308
|
+
return False
|
|
309
|
+
|
|
310
|
+
def inspect(self, container_id):
|
|
311
|
+
"""
|
|
312
|
+
Inspect a BoxLite box.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
container_id: Box ID or name
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Dict with box information or None if not found
|
|
319
|
+
"""
|
|
320
|
+
try:
|
|
321
|
+
box = self.runtime.get(container_id)
|
|
322
|
+
|
|
323
|
+
if box is None:
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
info = box.info()
|
|
327
|
+
ports = self.ports_cache.get(container_id) or []
|
|
328
|
+
|
|
329
|
+
# Convert BoxInfo to dict format similar to Docker
|
|
330
|
+
return {
|
|
331
|
+
"Id": info.id,
|
|
332
|
+
"Name": info.name or "",
|
|
333
|
+
"State": {
|
|
334
|
+
"Status": info.state.status,
|
|
335
|
+
"Running": info.state.running,
|
|
336
|
+
"Paused": False,
|
|
337
|
+
"Restarting": False,
|
|
338
|
+
"OOMKilled": False,
|
|
339
|
+
"Dead": not info.state.running,
|
|
340
|
+
"Pid": info.state.pid or 0,
|
|
341
|
+
"ExitCode": 0 if info.state.running else 1,
|
|
342
|
+
"Error": "",
|
|
343
|
+
"StartedAt": info.created_at,
|
|
344
|
+
"FinishedAt": ""
|
|
345
|
+
if info.state.running
|
|
346
|
+
else info.created_at,
|
|
347
|
+
},
|
|
348
|
+
"Created": info.created_at,
|
|
349
|
+
"Image": info.image,
|
|
350
|
+
"Config": {
|
|
351
|
+
"Env": [], # BoxInfo doesn't expose env directly
|
|
352
|
+
},
|
|
353
|
+
"NetworkSettings": {
|
|
354
|
+
"Ports": self._format_ports(ports),
|
|
355
|
+
},
|
|
356
|
+
"HostConfig": {
|
|
357
|
+
"CpuCount": info.cpus,
|
|
358
|
+
"Memory": info.memory_mib * 1024 * 1024,
|
|
359
|
+
# Convert MiB to bytes
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
except Exception as e:
|
|
363
|
+
logger.warning(f"An error occurred: {e}")
|
|
364
|
+
logger.debug(f"{traceback.format_exc()}")
|
|
365
|
+
return None
|
|
366
|
+
|
|
367
|
+
def get_status(self, container_id):
|
|
368
|
+
"""
|
|
369
|
+
Get the current status of the specified box.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
container_id: Box ID or name
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
str: Status string ('running', 'stopped', etc.) or None if not
|
|
376
|
+
found
|
|
377
|
+
"""
|
|
378
|
+
box_attrs = self.inspect(container_id=container_id)
|
|
379
|
+
if box_attrs:
|
|
380
|
+
return box_attrs["State"]["Status"]
|
|
381
|
+
return None
|
|
382
|
+
|
|
383
|
+
def _find_free_ports(self, n):
|
|
384
|
+
"""
|
|
385
|
+
Find n free ports in the configured port range.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
n: Number of ports to find
|
|
389
|
+
|
|
390
|
+
Returns:
|
|
391
|
+
List of free port numbers
|
|
392
|
+
|
|
393
|
+
Raises:
|
|
394
|
+
RuntimeError: If not enough free ports are available
|
|
395
|
+
"""
|
|
396
|
+
free_ports = []
|
|
397
|
+
|
|
398
|
+
for port in self.port_range:
|
|
399
|
+
if len(free_ports) >= n:
|
|
400
|
+
break # We have found enough ports
|
|
401
|
+
|
|
402
|
+
if not self.port_set.add(port):
|
|
403
|
+
continue # Port already in set
|
|
404
|
+
|
|
405
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
406
|
+
try:
|
|
407
|
+
s.bind(("", port))
|
|
408
|
+
free_ports.append(port) # Port is available
|
|
409
|
+
except OSError:
|
|
410
|
+
# Bind failed, port is in use
|
|
411
|
+
self.port_set.remove(port)
|
|
412
|
+
# Try the next one
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
if len(free_ports) < n:
|
|
416
|
+
raise RuntimeError(
|
|
417
|
+
"Not enough free ports available in the specified range.",
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
return free_ports
|
|
421
|
+
|
|
422
|
+
def _format_ports(self, host_ports):
|
|
423
|
+
"""
|
|
424
|
+
Format port list for Docker-like inspect output.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
host_ports: List of host port numbers
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Dict formatted like Docker's NetworkSettings.Ports
|
|
431
|
+
"""
|
|
432
|
+
if not host_ports:
|
|
433
|
+
return {}
|
|
434
|
+
|
|
435
|
+
ports = {}
|
|
436
|
+
for host_port in host_ports:
|
|
437
|
+
# We don't have the container port info here, so we'll use the
|
|
438
|
+
# host port as both host and container port
|
|
439
|
+
key = f"{host_port}/tcp"
|
|
440
|
+
ports[key] = [{"HostIp": "0.0.0.0", "HostPort": str(host_port)}]
|
|
441
|
+
|
|
442
|
+
return ports
|
|
@@ -17,26 +17,6 @@ from ..collections import (
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def is_port_available(port):
|
|
21
|
-
"""
|
|
22
|
-
Check if a given port is available (not in use) on the local system.
|
|
23
|
-
|
|
24
|
-
Args:
|
|
25
|
-
port (int): The port number to check.
|
|
26
|
-
|
|
27
|
-
Returns:
|
|
28
|
-
bool: True if the port is available, False if it is in use.
|
|
29
|
-
"""
|
|
30
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
31
|
-
try:
|
|
32
|
-
s.bind(("", port))
|
|
33
|
-
# Port is available
|
|
34
|
-
return True
|
|
35
|
-
except OSError:
|
|
36
|
-
# Port is in use
|
|
37
|
-
return False
|
|
38
|
-
|
|
39
|
-
|
|
40
20
|
class DockerClient(BaseClient):
|
|
41
21
|
def __init__(self, config=None):
|
|
42
22
|
self.config = config
|
|
@@ -827,13 +827,15 @@ class FCClient(BaseClient):
|
|
|
827
827
|
"""
|
|
828
828
|
replacement_map = {
|
|
829
829
|
"agentscope/runtime-sandbox-base": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
830
|
-
"/agentscope_runtime-sandbox-base:
|
|
830
|
+
"/agentscope_runtime-sandbox-base:20260106",
|
|
831
831
|
"agentscope/runtime-sandbox-browser": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
832
|
-
"/agentscope_runtime-sandbox-browser:
|
|
832
|
+
"/agentscope_runtime-sandbox-browser:20260106",
|
|
833
833
|
"agentscope/runtime-sandbox-filesystem": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
834
|
-
"/agentscope_runtime-sandbox-filesystem:
|
|
834
|
+
"/agentscope_runtime-sandbox-filesystem:20260106",
|
|
835
835
|
"agentscope/runtime-sandbox-gui": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
836
|
-
"/agentscope_runtime-sandbox-gui:
|
|
836
|
+
"/agentscope_runtime-sandbox-gui:20260106",
|
|
837
|
+
"agentscope/runtime-sandbox-mobile": "serverless-registry.cn-hangzhou.cr.aliyuncs.com/functionai" # noqa: E501
|
|
838
|
+
"/agentscope_runtime-sandbox-mobile:20251217",
|
|
837
839
|
}
|
|
838
840
|
|
|
839
841
|
if ":" in image:
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
from .docker_client import DockerClient
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GVisorDockerClient(DockerClient):
|
|
9
|
+
"""
|
|
10
|
+
A DockerClient that enforces gVisor runtime (`runsc`).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def create(
|
|
14
|
+
self,
|
|
15
|
+
image,
|
|
16
|
+
name=None,
|
|
17
|
+
ports=None,
|
|
18
|
+
volumes=None,
|
|
19
|
+
environment=None,
|
|
20
|
+
runtime_config=None,
|
|
21
|
+
):
|
|
22
|
+
if runtime_config is None:
|
|
23
|
+
runtime_config = {}
|
|
24
|
+
|
|
25
|
+
runtime_config["runtime"] = "runsc"
|
|
26
|
+
|
|
27
|
+
logger.debug(
|
|
28
|
+
f"[GVisorDockerClient] Forcing runtime=runsc for image {image}",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return super().create(
|
|
32
|
+
image=image,
|
|
33
|
+
name=name,
|
|
34
|
+
ports=ports,
|
|
35
|
+
volumes=volumes,
|
|
36
|
+
environment=environment,
|
|
37
|
+
runtime_config=runtime_config,
|
|
38
|
+
)
|