Skip to content

Commit 92c1998

Browse files
liyuanliyuan
authored andcommitted
feat(sandbox): abstract interface for sandbox service
Add abstract base classes for sandbox lifecycle management: - Sandbox: Interface for sandbox instance operations - SandboxService: Interface for sandbox lifecycle management - Support for template-based creation (image/snapshot) - File operations (upload/download/read/write) - Code execution (Python/JavaScript) - Snapshot management
1 parent ce37139 commit 92c1998

File tree

2 files changed

+376
-0
lines changed

2 files changed

+376
-0
lines changed

src/xagent/sandbox/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"""
2+
Sandbox Support.
3+
"""
4+
5+
from xagent.sandbox.base import (
6+
CodeType,
7+
ExecResult,
8+
Sandbox,
9+
SandboxConfig,
10+
SandboxInfo,
11+
SandboxService,
12+
SandboxSnapshot,
13+
SandBoxTemplate,
14+
TemplateType,
15+
)
16+
17+
__all__ = [
18+
"TemplateType",
19+
"CodeType",
20+
"SandBoxTemplate",
21+
"SandboxConfig",
22+
"SandboxInfo",
23+
"SandboxSnapshot",
24+
"ExecResult",
25+
"Sandbox",
26+
"SandboxService",
27+
]

src/xagent/sandbox/base.py

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
"""
2+
Abstract interface for Sandbox Service.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import abc
8+
from dataclasses import dataclass, field
9+
from typing import Literal, Optional
10+
11+
TemplateType = Literal["image", "snapshot"]
12+
"""Supported template types."""
13+
14+
CodeType = Literal["python", "javascript"]
15+
"""Supported code execution types."""
16+
17+
18+
@dataclass
19+
class SandBoxTemplate:
20+
"""
21+
Template for creating a sandbox.
22+
"""
23+
24+
_type: Optional[TemplateType] = "image"
25+
"""Template type."""
26+
27+
image: Optional[str] = None
28+
"""Container image, required when _type=image."""
29+
30+
snapshot_id: Optional[str] = None
31+
"""Snapshot ID, required when _type=snapshot."""
32+
33+
34+
@dataclass
35+
class SandboxConfig:
36+
"""
37+
Configuration parameters for creating a sandbox.
38+
"""
39+
40+
cpus: Optional[int] = 1
41+
"""CPU core limit."""
42+
43+
memory: Optional[int] = 512
44+
"""Memory limit in MB."""
45+
46+
env: Optional[dict[str, str]] = None
47+
"""Environment variables to inject."""
48+
49+
volumes: Optional[list[tuple[str, str, str]]] = None
50+
"""Volume mounts as (host_path, guest_path, mode).
51+
Mode: 'ro' (read-only) or 'rw' (read-write)."""
52+
53+
network_isolated: Optional[bool] = False
54+
"""Network isolation. True blocks external network access."""
55+
56+
ports: Optional[list[tuple[int, int]]] = None
57+
"""Port mappings as [(host_port, guest_port)]."""
58+
59+
60+
@dataclass
61+
class SandboxInfo:
62+
"""Sandbox status information."""
63+
64+
name: str
65+
"""Sandbox name."""
66+
67+
state: str
68+
"""Sandbox state:
69+
- 'running': Running
70+
- 'stopped': Stopped
71+
- 'unknown': Unknown
72+
"""
73+
74+
template: SandBoxTemplate
75+
"""Template used to create this sandbox."""
76+
77+
config: SandboxConfig
78+
"""Configuration used to create this sandbox."""
79+
80+
created_at: Optional[str] = None
81+
"""Creation time in ISO 8601 format."""
82+
83+
84+
@dataclass
85+
class SandboxSnapshot:
86+
"""Sandbox snapshot information."""
87+
88+
snapshot_id: str
89+
"""Snapshot ID."""
90+
91+
metadata: dict = field(default_factory=dict)
92+
"""Snapshot metadata."""
93+
94+
created_at: Optional[str] = None
95+
"""Creation time in ISO 8601 format."""
96+
97+
98+
@dataclass
99+
class ExecResult:
100+
"""Execution result of a command or code."""
101+
102+
exit_code: int
103+
"""Exit code. 0 indicates success, non-zero indicates failure."""
104+
105+
stdout: str
106+
"""Standard output."""
107+
108+
stderr: str
109+
"""Standard error output."""
110+
111+
@property
112+
def success(self) -> bool:
113+
return self.exit_code == 0
114+
115+
116+
class Sandbox(abc.ABC):
117+
"""
118+
Abstract interface for a sandbox instance.
119+
120+
Supports two usage patterns:
121+
122+
# Manual stop
123+
try:
124+
result = await sandbox.exec("echo hello")
125+
finally:
126+
await sandbox.stop()
127+
128+
# Auto-stop with async context manager
129+
async with sandbox:
130+
result = await sandbox.exec("echo hello")
131+
"""
132+
133+
async def __aenter__(self) -> "Sandbox":
134+
return self
135+
136+
async def __aexit__(
137+
self,
138+
exc_type: type[BaseException] | None,
139+
exc_val: BaseException | None,
140+
exc_tb: object,
141+
) -> None:
142+
await self.stop()
143+
144+
# --- Properties ---
145+
146+
@property
147+
@abc.abstractmethod
148+
def name(self) -> str:
149+
"""Sandbox name (unique identifier)."""
150+
151+
# --- Lifecycle ---
152+
153+
@abc.abstractmethod
154+
async def stop(self) -> None:
155+
"""Stop the sandbox, preserving its state."""
156+
157+
@abc.abstractmethod
158+
async def info(self) -> SandboxInfo:
159+
"""Get sandbox status information."""
160+
161+
# --- Execution ---
162+
163+
@abc.abstractmethod
164+
async def exec(
165+
self,
166+
command: str,
167+
*args: str,
168+
env: Optional[dict[str, str]] = None,
169+
) -> ExecResult:
170+
"""Execute a shell command in the sandbox.
171+
172+
Args:
173+
command: Shell command to execute.
174+
args: Command arguments.
175+
env: Additional environment variables (merged with existing).
176+
177+
Returns:
178+
ExecResult: Execution result with exit code, stdout, and stderr.
179+
"""
180+
181+
@abc.abstractmethod
182+
async def run_code(
183+
self,
184+
code: str,
185+
code_type: CodeType = "python",
186+
env: Optional[dict[str, str]] = None,
187+
) -> ExecResult:
188+
"""Execute code in the sandbox.
189+
190+
Args:
191+
code: Code string to execute.
192+
code_type: Code type.
193+
env: Additional environment variables (merged with existing).
194+
195+
Returns:
196+
ExecResult: Execution result with exit code, stdout, and stderr.
197+
"""
198+
199+
# --- File Operations ---
200+
201+
@abc.abstractmethod
202+
async def upload_file(
203+
self, local_path: str, remote_path: str, overwrite: bool = False
204+
) -> None:
205+
"""Upload a local file to the sandbox.
206+
207+
Args:
208+
local_path: Local file path.
209+
remote_path: Target path in sandbox (including filename).
210+
overwrite: Whether to overwrite if target exists. Default False.
211+
212+
Raises:
213+
FileNotFoundError: Local file not found.
214+
FileExistsError: Target exists and overwrite=False.
215+
"""
216+
217+
@abc.abstractmethod
218+
async def download_file(
219+
self, remote_path: str, local_path: str, overwrite: bool = False
220+
) -> None:
221+
"""Download a file from the sandbox.
222+
223+
Args:
224+
remote_path: Source path in sandbox.
225+
local_path: Local target path (including filename).
226+
overwrite: Whether to overwrite if local file exists. Default False.
227+
228+
Raises:
229+
FileNotFoundError: Source file not found in sandbox.
230+
FileExistsError: Local file exists and overwrite=False.
231+
"""
232+
233+
@abc.abstractmethod
234+
async def write_file(
235+
self, content: str, remote_path: str, overwrite: bool = False
236+
) -> None:
237+
"""Write string content directly to a sandbox file.
238+
239+
Args:
240+
content: Text content to write.
241+
remote_path: Target path in sandbox (including filename).
242+
overwrite: Whether to overwrite if target exists. Default False.
243+
244+
Raises:
245+
FileExistsError: Target exists and overwrite=False.
246+
"""
247+
248+
@abc.abstractmethod
249+
async def read_file(self, remote_path: str) -> str:
250+
"""Read file content from the sandbox.
251+
252+
Args:
253+
remote_path: File path in sandbox.
254+
255+
Raises:
256+
FileNotFoundError: File not found in sandbox.
257+
"""
258+
259+
260+
class SandboxService(abc.ABC):
261+
"""
262+
Abstract interface for sandbox lifecycle management.
263+
264+
Typical usage:
265+
266+
service = BoxliteService()
267+
268+
# Get or create sandbox
269+
async with await service.get_or_create("my-box") as sandbox:
270+
result = await sandbox.exec("python train.py")
271+
print(sandbox.name) # "my-box"
272+
273+
# List all sandboxes
274+
boxes = await service.list_sandboxes()
275+
print(boxes)
276+
277+
# Delete sandbox
278+
await service.delete("my-box")
279+
280+
# Create snapshot
281+
await service.create_snapshot("my-box", "my-box-v1.0")
282+
283+
# Create from snapshot
284+
await service.get_or_create("my-box", template=SandBoxTemplate(_type="snapshot", snapshot_id="my-box-v1.0"))
285+
"""
286+
287+
@abc.abstractmethod
288+
async def get_or_create(
289+
self,
290+
name: str,
291+
template: Optional[SandBoxTemplate] = None,
292+
config: Optional[SandboxConfig] = None,
293+
) -> Sandbox:
294+
"""Get or create a sandbox, handling resume automatically.
295+
296+
Behavior:
297+
- Exists and running → return directly
298+
- Exists and stopped → resume and return
299+
- Does not exist → create and return
300+
301+
Args:
302+
name: Sandbox name (unique identifier).
303+
template: Template for creation only. Ignored for existing sandboxes.
304+
config: Configuration for creation only. Ignored for existing sandboxes.
305+
306+
Returns:
307+
Sandbox: Operational sandbox instance.
308+
"""
309+
310+
@abc.abstractmethod
311+
async def list_sandboxes(self) -> list[SandboxInfo]:
312+
"""List all sandboxes (both running and stopped).
313+
314+
Returns:
315+
list[SandboxInfo]: List of sandbox status information.
316+
"""
317+
318+
@abc.abstractmethod
319+
async def delete(self, name: str) -> None:
320+
"""Permanently delete a sandbox and release all resources.
321+
322+
Args:
323+
name: Sandbox name to delete.
324+
"""
325+
326+
@abc.abstractmethod
327+
async def create_snapshot(self, name: str, snapshot_id: str) -> SandboxSnapshot:
328+
"""Create a sandbox snapshot.
329+
330+
Args:
331+
name: Sandbox name.
332+
snapshot_id: Unique snapshot identifier.
333+
"""
334+
335+
@abc.abstractmethod
336+
async def list_snapshot(self) -> list[SandboxSnapshot]:
337+
"""List all sandbox snapshots.
338+
339+
Returns:
340+
list[SandboxSnapshot]: List of snapshot information.
341+
"""
342+
343+
@abc.abstractmethod
344+
async def delete_snapshot(self, snapshot_id: str) -> None:
345+
"""Permanently delete a sandbox snapshot.
346+
347+
Args:
348+
snapshot_id: Unique snapshot identifier.
349+
"""

0 commit comments

Comments
 (0)