flash-sandbox 0.1.0__tar.gz

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.
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: flash-sandbox
3
+ Version: 0.1.0
4
+ Summary: A Python SDK for interacting with the Sandbox Orchestrator.
5
+ Requires-Python: >=3.8
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: grpcio>=1.50.0
8
+ Requires-Dist: protobuf>=4.21.0
9
+ Requires-Dist: requests>=2.28.0
10
+ Provides-Extra: dev
11
+ Requires-Dist: grpcio-tools>=1.50.0; extra == "dev"
12
+ Requires-Dist: pytest; extra == "dev"
13
+
14
+ # Sandbox SDK
15
+
16
+ A Python SDK for interacting with the Sandbox Orchestrator. Supports both
17
+ **gRPC** and **HTTP** transports with an identical public interface, so you can swap protocols without changing application logic.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pip install flash-sandbox
23
+ ```
24
+
25
+ The package pulls in `grpcio`, `protobuf`, and `requests` automatically.
26
+
27
+ ## Quick Start
28
+
29
+ ### HTTP transport (recommended for simplicity)
30
+
31
+ ```python
32
+ from flash_sandbox import HTTPClient
33
+
34
+ client = HTTPClient(host="localhost", port=8080)
35
+
36
+ # Start a Docker sandbox
37
+ sandbox_id = client.start_sandbox(
38
+ type="docker",
39
+ image="alpine:latest",
40
+ command=["tail", "-f", "/dev/null"],
41
+ memory_mb=128,
42
+ cpu_cores=0.5,
43
+ )
44
+ print(f"Sandbox ID: {sandbox_id}")
45
+
46
+ # Execute a command
47
+ result = client.exec_command(sandbox_id, ["echo", "Hello from HTTP!"])
48
+ print(f"Output: {result.stdout.strip()}")
49
+ print(f"Exit code: {result.exit_code}")
50
+
51
+ # Check status
52
+ status = client.get_status(sandbox_id)
53
+ print(f"Status: {status}")
54
+
55
+ # Get resource metrics
56
+ metrics = client.get_metrics(sandbox_id)
57
+ print(f"Memory: {metrics.memory_usage_bytes} / {metrics.memory_limit_bytes}")
58
+ print(f"CPU: {metrics.cpu_percent}%")
59
+
60
+ # Snapshot and resume
61
+ snap = client.snapshot_sandbox(sandbox_id)
62
+ print(f"Snapshot: {snap['snapshot_path']}")
63
+ client.resume_sandbox(sandbox_id)
64
+
65
+ # Stop
66
+ client.stop_sandbox(sandbox_id)
67
+ client.close()
68
+ ```
69
+
70
+ ### gRPC transport
71
+
72
+ ```python
73
+ from flash_sandbox import SandboxClient
74
+
75
+ client = SandboxClient(host="localhost", port=50051)
76
+
77
+ # Start a Docker sandbox
78
+ docker_id = client.start_sandbox(
79
+ type="docker",
80
+ image="alpine:latest",
81
+ command=["tail", "-f", "/dev/null"],
82
+ memory_mb=128,
83
+ cpu_cores=0.5,
84
+ )
85
+ print(f"Docker Sandbox ID: {docker_id}")
86
+
87
+ # Start a Firecracker sandbox
88
+ fc_id = client.start_sandbox(
89
+ type="firecracker",
90
+ image="alpine:latest",
91
+ command=["tail", "-f", "/dev/null"],
92
+ memory_mb=512,
93
+ cpu_cores=1.0,
94
+ )
95
+ print(f"Firecracker Sandbox ID: {fc_id}")
96
+
97
+ # Start a gVisor sandbox
98
+ gvisor_id = client.start_sandbox(
99
+ type="gvisor",
100
+ image="alpine:latest",
101
+ command=["tail", "-f", "/dev/null"],
102
+ memory_mb=256,
103
+ cpu_cores=1.0,
104
+ )
105
+ print(f"gVisor Sandbox ID: {gvisor_id}")
106
+
107
+ # Check status
108
+ status = client.get_status(fc_id)
109
+ print(f"Firecracker Status: {status}")
110
+
111
+ # Snapshot and Resume
112
+ snap_res = client.snapshot_sandbox(fc_id)
113
+ print(f"Snapshot Data: {snap_res}")
114
+ client.resume_sandbox(fc_id)
115
+
116
+ # Execute commands
117
+ exec_res = client.exec_command(gvisor_id, ["echo", "Hello from gVisor!"])
118
+ print(f"gVisor Output: {exec_res.stdout.strip()}")
119
+ print(f"gVisor Exit Code: {exec_res.exit_code}")
120
+
121
+ # Stop sandboxes
122
+ client.stop_sandbox(docker_id)
123
+ client.stop_sandbox(fc_id)
124
+ client.stop_sandbox(gvisor_id)
125
+ client.close()
126
+ ```
127
+
128
+ ## API Reference
129
+
130
+ Both `HTTPClient` and `SandboxClient` expose the same set of methods:
131
+
132
+ | Method | Description |
133
+ |---|---|
134
+ | `start_sandbox(type, image, command, memory_mb, cpu_cores, ...)` | Start a new sandbox and return its ID. |
135
+ | `stop_sandbox(sandbox_id)` | Stop and remove a running sandbox. |
136
+ | `exec_command(sandbox_id, command)` | Execute a command in a sandbox. Returns an object with `stdout`, `stderr`, and `exit_code`. |
137
+ | `get_status(sandbox_id)` | Return the status string (`"running"`, `"stopped"`, etc.). |
138
+ | `get_metrics(sandbox_id)` | Return point-in-time resource-usage metrics (memory, CPU, network, block I/O). |
139
+ | `snapshot_sandbox(sandbox_id)` | Create a snapshot. Returns `{"snapshot_path": ..., "mem_file_path": ...}`. |
140
+ | `resume_sandbox(sandbox_id)` | Resume a paused / snapshotted sandbox. |
141
+ | `close()` | Release the underlying connection / session. |
142
+
143
+ ### Constructor options
144
+
145
+ #### HTTPClient
146
+
147
+ ```python
148
+ HTTPClient(
149
+ host="localhost", # Orchestrator hostname
150
+ port=8080, # HTTP port (default 8080)
151
+ address=None, # Full URL, overrides host/port (e.g. "http://proxy:9090/v1/service/sandbox")
152
+ timeout=30.0, # Request timeout in seconds (None = no timeout)
153
+ session=None, # Optional requests.Session for custom TLS / auth / retries
154
+ )
155
+ ```
156
+
157
+ #### SandboxClient (gRPC)
158
+
159
+ ```python
160
+ SandboxClient(
161
+ host="localhost", # Orchestrator hostname
162
+ port=50051, # gRPC port (default 50051)
163
+ address=None, # Full address for proxy routing (e.g. "localhost:8092/v1/service/sandbox")
164
+ )
165
+ ```
166
+
167
+ ### HTTPClient-only extras
168
+
169
+ The HTTP transport includes a few additional features:
170
+
171
+ - **Firecracker fields** on `start_sandbox`: `kernel_image`, `initrd_path`, `snapshot_path`, `mem_file_path`.
172
+ - **Custom timeouts** per-client via the `timeout` parameter.
173
+ - **Session injection** – pass your own `requests.Session` for connection pooling, mutual TLS, retry policies, or authentication headers.
174
+ - **Typed exceptions**: `SandboxHTTPError` (with `.status_code` and `.detail`) and the more specific `SandboxNotFoundError` for 404 responses.
175
+
176
+ ### Response types (HTTP client)
177
+
178
+ | Class | Fields |
179
+ |---|---|
180
+ | `ExecResult` | `stdout: str`, `stderr: str`, `exit_code: int` |
181
+ | `MetricsResult` | `memory_usage_bytes`, `memory_limit_bytes`, `memory_percent`, `cpu_percent`, `pids_current`, `net_rx_bytes`, `net_tx_bytes`, `block_read_bytes`, `block_write_bytes` |
182
+ | `SnapshotResult` | `snapshot_path: str`, `mem_file_path: str` |
183
+
184
+ All response dataclasses are **frozen** (immutable).
185
+
186
+ ## Context manager
187
+
188
+ Both clients support the context-manager protocol:
189
+
190
+ ```python
191
+ from flash_sandbox import HTTPClient
192
+
193
+ with HTTPClient(host="localhost") as client:
194
+ sid = client.start_sandbox(type="docker", image="alpine:latest")
195
+ client.stop_sandbox(sid)
196
+ # Connection is automatically closed here.
197
+ ```
198
+
199
+ ## Proxy / reverse-proxy support
200
+
201
+ Both clients support routing through a reverse proxy that uses path-based
202
+ routing. Pass the full address including the path prefix:
203
+
204
+ ```python
205
+ # HTTP through a proxy
206
+ http_client = HTTPClient(address="http://proxy.example.com:8092/v1/service/sandbox")
207
+
208
+ # gRPC through a proxy
209
+ grpc_client = SandboxClient(address="proxy.example.com:8092/v1/service/sandbox")
210
+ ```
211
+
212
+ ## Running tests
213
+
214
+ ```bash
215
+ pip install -e ".[dev]"
216
+ pytest tests/
217
+ ```
@@ -0,0 +1,204 @@
1
+ # Sandbox SDK
2
+
3
+ A Python SDK for interacting with the Sandbox Orchestrator. Supports both
4
+ **gRPC** and **HTTP** transports with an identical public interface, so you can swap protocols without changing application logic.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install flash-sandbox
10
+ ```
11
+
12
+ The package pulls in `grpcio`, `protobuf`, and `requests` automatically.
13
+
14
+ ## Quick Start
15
+
16
+ ### HTTP transport (recommended for simplicity)
17
+
18
+ ```python
19
+ from flash_sandbox import HTTPClient
20
+
21
+ client = HTTPClient(host="localhost", port=8080)
22
+
23
+ # Start a Docker sandbox
24
+ sandbox_id = client.start_sandbox(
25
+ type="docker",
26
+ image="alpine:latest",
27
+ command=["tail", "-f", "/dev/null"],
28
+ memory_mb=128,
29
+ cpu_cores=0.5,
30
+ )
31
+ print(f"Sandbox ID: {sandbox_id}")
32
+
33
+ # Execute a command
34
+ result = client.exec_command(sandbox_id, ["echo", "Hello from HTTP!"])
35
+ print(f"Output: {result.stdout.strip()}")
36
+ print(f"Exit code: {result.exit_code}")
37
+
38
+ # Check status
39
+ status = client.get_status(sandbox_id)
40
+ print(f"Status: {status}")
41
+
42
+ # Get resource metrics
43
+ metrics = client.get_metrics(sandbox_id)
44
+ print(f"Memory: {metrics.memory_usage_bytes} / {metrics.memory_limit_bytes}")
45
+ print(f"CPU: {metrics.cpu_percent}%")
46
+
47
+ # Snapshot and resume
48
+ snap = client.snapshot_sandbox(sandbox_id)
49
+ print(f"Snapshot: {snap['snapshot_path']}")
50
+ client.resume_sandbox(sandbox_id)
51
+
52
+ # Stop
53
+ client.stop_sandbox(sandbox_id)
54
+ client.close()
55
+ ```
56
+
57
+ ### gRPC transport
58
+
59
+ ```python
60
+ from flash_sandbox import SandboxClient
61
+
62
+ client = SandboxClient(host="localhost", port=50051)
63
+
64
+ # Start a Docker sandbox
65
+ docker_id = client.start_sandbox(
66
+ type="docker",
67
+ image="alpine:latest",
68
+ command=["tail", "-f", "/dev/null"],
69
+ memory_mb=128,
70
+ cpu_cores=0.5,
71
+ )
72
+ print(f"Docker Sandbox ID: {docker_id}")
73
+
74
+ # Start a Firecracker sandbox
75
+ fc_id = client.start_sandbox(
76
+ type="firecracker",
77
+ image="alpine:latest",
78
+ command=["tail", "-f", "/dev/null"],
79
+ memory_mb=512,
80
+ cpu_cores=1.0,
81
+ )
82
+ print(f"Firecracker Sandbox ID: {fc_id}")
83
+
84
+ # Start a gVisor sandbox
85
+ gvisor_id = client.start_sandbox(
86
+ type="gvisor",
87
+ image="alpine:latest",
88
+ command=["tail", "-f", "/dev/null"],
89
+ memory_mb=256,
90
+ cpu_cores=1.0,
91
+ )
92
+ print(f"gVisor Sandbox ID: {gvisor_id}")
93
+
94
+ # Check status
95
+ status = client.get_status(fc_id)
96
+ print(f"Firecracker Status: {status}")
97
+
98
+ # Snapshot and Resume
99
+ snap_res = client.snapshot_sandbox(fc_id)
100
+ print(f"Snapshot Data: {snap_res}")
101
+ client.resume_sandbox(fc_id)
102
+
103
+ # Execute commands
104
+ exec_res = client.exec_command(gvisor_id, ["echo", "Hello from gVisor!"])
105
+ print(f"gVisor Output: {exec_res.stdout.strip()}")
106
+ print(f"gVisor Exit Code: {exec_res.exit_code}")
107
+
108
+ # Stop sandboxes
109
+ client.stop_sandbox(docker_id)
110
+ client.stop_sandbox(fc_id)
111
+ client.stop_sandbox(gvisor_id)
112
+ client.close()
113
+ ```
114
+
115
+ ## API Reference
116
+
117
+ Both `HTTPClient` and `SandboxClient` expose the same set of methods:
118
+
119
+ | Method | Description |
120
+ |---|---|
121
+ | `start_sandbox(type, image, command, memory_mb, cpu_cores, ...)` | Start a new sandbox and return its ID. |
122
+ | `stop_sandbox(sandbox_id)` | Stop and remove a running sandbox. |
123
+ | `exec_command(sandbox_id, command)` | Execute a command in a sandbox. Returns an object with `stdout`, `stderr`, and `exit_code`. |
124
+ | `get_status(sandbox_id)` | Return the status string (`"running"`, `"stopped"`, etc.). |
125
+ | `get_metrics(sandbox_id)` | Return point-in-time resource-usage metrics (memory, CPU, network, block I/O). |
126
+ | `snapshot_sandbox(sandbox_id)` | Create a snapshot. Returns `{"snapshot_path": ..., "mem_file_path": ...}`. |
127
+ | `resume_sandbox(sandbox_id)` | Resume a paused / snapshotted sandbox. |
128
+ | `close()` | Release the underlying connection / session. |
129
+
130
+ ### Constructor options
131
+
132
+ #### HTTPClient
133
+
134
+ ```python
135
+ HTTPClient(
136
+ host="localhost", # Orchestrator hostname
137
+ port=8080, # HTTP port (default 8080)
138
+ address=None, # Full URL, overrides host/port (e.g. "http://proxy:9090/v1/service/sandbox")
139
+ timeout=30.0, # Request timeout in seconds (None = no timeout)
140
+ session=None, # Optional requests.Session for custom TLS / auth / retries
141
+ )
142
+ ```
143
+
144
+ #### SandboxClient (gRPC)
145
+
146
+ ```python
147
+ SandboxClient(
148
+ host="localhost", # Orchestrator hostname
149
+ port=50051, # gRPC port (default 50051)
150
+ address=None, # Full address for proxy routing (e.g. "localhost:8092/v1/service/sandbox")
151
+ )
152
+ ```
153
+
154
+ ### HTTPClient-only extras
155
+
156
+ The HTTP transport includes a few additional features:
157
+
158
+ - **Firecracker fields** on `start_sandbox`: `kernel_image`, `initrd_path`, `snapshot_path`, `mem_file_path`.
159
+ - **Custom timeouts** per-client via the `timeout` parameter.
160
+ - **Session injection** – pass your own `requests.Session` for connection pooling, mutual TLS, retry policies, or authentication headers.
161
+ - **Typed exceptions**: `SandboxHTTPError` (with `.status_code` and `.detail`) and the more specific `SandboxNotFoundError` for 404 responses.
162
+
163
+ ### Response types (HTTP client)
164
+
165
+ | Class | Fields |
166
+ |---|---|
167
+ | `ExecResult` | `stdout: str`, `stderr: str`, `exit_code: int` |
168
+ | `MetricsResult` | `memory_usage_bytes`, `memory_limit_bytes`, `memory_percent`, `cpu_percent`, `pids_current`, `net_rx_bytes`, `net_tx_bytes`, `block_read_bytes`, `block_write_bytes` |
169
+ | `SnapshotResult` | `snapshot_path: str`, `mem_file_path: str` |
170
+
171
+ All response dataclasses are **frozen** (immutable).
172
+
173
+ ## Context manager
174
+
175
+ Both clients support the context-manager protocol:
176
+
177
+ ```python
178
+ from flash_sandbox import HTTPClient
179
+
180
+ with HTTPClient(host="localhost") as client:
181
+ sid = client.start_sandbox(type="docker", image="alpine:latest")
182
+ client.stop_sandbox(sid)
183
+ # Connection is automatically closed here.
184
+ ```
185
+
186
+ ## Proxy / reverse-proxy support
187
+
188
+ Both clients support routing through a reverse proxy that uses path-based
189
+ routing. Pass the full address including the path prefix:
190
+
191
+ ```python
192
+ # HTTP through a proxy
193
+ http_client = HTTPClient(address="http://proxy.example.com:8092/v1/service/sandbox")
194
+
195
+ # gRPC through a proxy
196
+ grpc_client = SandboxClient(address="proxy.example.com:8092/v1/service/sandbox")
197
+ ```
198
+
199
+ ## Running tests
200
+
201
+ ```bash
202
+ pip install -e ".[dev]"
203
+ pytest tests/
204
+ ```
@@ -0,0 +1,12 @@
1
+ from .client import SandboxClient
2
+ from .http_client import HTTPClient, ExecResult, MetricsResult, SnapshotResult, SandboxHTTPError, SandboxNotFoundError
3
+
4
+ __all__ = [
5
+ "SandboxClient",
6
+ "HTTPClient",
7
+ "ExecResult",
8
+ "MetricsResult",
9
+ "SnapshotResult",
10
+ "SandboxHTTPError",
11
+ "SandboxNotFoundError",
12
+ ]
@@ -0,0 +1,220 @@
1
+ import grpc
2
+ from typing import List, Optional
3
+ from urllib.parse import urlparse
4
+
5
+ from .proto import sandbox_pb2
6
+ from .proto import sandbox_pb2_grpc
7
+
8
+
9
+ class _OCFProxyInterceptor(
10
+ grpc.UnaryUnaryClientInterceptor,
11
+ grpc.UnaryStreamClientInterceptor,
12
+ grpc.StreamUnaryClientInterceptor,
13
+ grpc.StreamStreamClientInterceptor,
14
+ ):
15
+ """Interceptor that prepends an OCF proxy path prefix to gRPC method paths.
16
+
17
+ Python's grpc library does NOT include the URL path portion of the channel
18
+ target in the HTTP/2 :path pseudo-header. It always sends
19
+ :path = /package.Service/Method. The OCF proxy needs the full path
20
+ (e.g. /v1/service/sandbox/package.Service/Method) to route the request.
21
+ This interceptor rewrites every outgoing RPC's method string to include
22
+ the prefix.
23
+ """
24
+
25
+ def __init__(self, path_prefix: str):
26
+ # Ensure the prefix starts with / and does not end with /
27
+ self._prefix = "/" + path_prefix.strip("/")
28
+
29
+ def _rewrite(self, client_call_details):
30
+ """Return a new ClientCallDetails with the rewritten method path."""
31
+ new_method = self._prefix + client_call_details.method
32
+ return _new_client_call_details(client_call_details, new_method)
33
+
34
+ def intercept_unary_unary(self, continuation, client_call_details, request):
35
+ return continuation(self._rewrite(client_call_details), request)
36
+
37
+ def intercept_unary_stream(self, continuation, client_call_details, request):
38
+ return continuation(self._rewrite(client_call_details), request)
39
+
40
+ def intercept_stream_unary(self, continuation, client_call_details, request_iterator):
41
+ return continuation(self._rewrite(client_call_details), request_iterator)
42
+
43
+ def intercept_stream_stream(self, continuation, client_call_details, request_iterator):
44
+ return continuation(self._rewrite(client_call_details), request_iterator)
45
+
46
+
47
+ class _ClientCallDetails(
48
+ grpc.ClientCallDetails,
49
+ ):
50
+ """Concrete implementation of ClientCallDetails for rewriting."""
51
+
52
+ def __init__(self, method, timeout, metadata, credentials, wait_for_ready, compression):
53
+ self.method = method
54
+ self.timeout = timeout
55
+ self.metadata = metadata
56
+ self.credentials = credentials
57
+ self.wait_for_ready = wait_for_ready
58
+ self.compression = compression
59
+
60
+
61
+ def _new_client_call_details(original, new_method):
62
+ return _ClientCallDetails(
63
+ method=new_method,
64
+ timeout=original.timeout,
65
+ metadata=original.metadata,
66
+ credentials=original.credentials,
67
+ wait_for_ready=original.wait_for_ready,
68
+ compression=original.compression,
69
+ )
70
+
71
+
72
+ class SandboxClient:
73
+ """Client for interacting with the Agent Sandbox Orchestrator via gRPC."""
74
+
75
+ def __init__(self, host: Optional[str] = None, port: int = 50051, address: Optional[str] = None):
76
+ """
77
+ Initialize the SandboxClient.
78
+
79
+ Args:
80
+ host: The hostname or IP address of the orchestrator.
81
+ port: The port number of the orchestrator gRPC service.
82
+ address: Full address for proxy-based routing (e.g.
83
+ ``localhost:8092/v1/service/sandbox``). The path portion
84
+ is extracted and prepended to each gRPC method via an
85
+ interceptor so the OCF proxy can route the request.
86
+ Overrides *host* and *port*.
87
+ """
88
+ if address:
89
+ # Strip scheme if the user passed one — gRPC channels don't use schemes.
90
+ addr = address
91
+ for scheme in ("http://", "https://"):
92
+ if addr.startswith(scheme):
93
+ addr = addr[len(scheme):]
94
+ break
95
+ # Split "host:port/path..." into the channel target and the proxy path.
96
+ # urlparse needs a scheme, so we add one temporarily.
97
+ parsed = urlparse(f"http://{addr}")
98
+ channel_target = f"{parsed.hostname}:{parsed.port or 80}"
99
+ proxy_path = parsed.path # e.g. "/v1/service/sandbox"
100
+ elif host:
101
+ channel_target = f"{host}:{port}"
102
+ proxy_path = ""
103
+ else:
104
+ raise ValueError("Must provide either an address or a host")
105
+
106
+ self.address = channel_target
107
+ self.channel = grpc.insecure_channel(channel_target)
108
+
109
+ if proxy_path and proxy_path != "/":
110
+ interceptor = _OCFProxyInterceptor(proxy_path)
111
+ self.channel = grpc.intercept_channel(self.channel, interceptor)
112
+
113
+ self.stub = sandbox_pb2_grpc.SandboxServiceStub(self.channel)
114
+
115
+ def close(self):
116
+ """Close the gRPC channel."""
117
+ self.channel.close()
118
+
119
+ def __enter__(self):
120
+ return self
121
+
122
+ def __exit__(self, exc_type, exc_value, traceback):
123
+ self.close()
124
+
125
+ def start_sandbox(
126
+ self,
127
+ type: str,
128
+ image: str,
129
+ command: Optional[List[str]] = None,
130
+ memory_mb: int = 512,
131
+ cpu_cores: float = 1.0,
132
+ ) -> str:
133
+ """
134
+ Start a new sandbox.
135
+
136
+ Args:
137
+ type: The type of sandbox (e.g., "docker", "firecracker").
138
+ image: The image to use for the sandbox.
139
+ command: The command to run in the sandbox.
140
+ memory_mb: Memory limit in MB.
141
+ cpu_cores: CPU cores limit.
142
+
143
+ Returns:
144
+ The ID of the started sandbox.
145
+ """
146
+ request = sandbox_pb2.StartRequest(
147
+ type=type,
148
+ image=image,
149
+ command=command or [],
150
+ memory_mb=memory_mb,
151
+ cpu_cores=cpu_cores,
152
+ )
153
+ response = self.stub.StartSandbox(request)
154
+ return response.id
155
+
156
+ def stop_sandbox(self, sandbox_id: str) -> None:
157
+ """
158
+ Stop a running sandbox.
159
+
160
+ Args:
161
+ sandbox_id: The ID of the sandbox to stop.
162
+ """
163
+ request = sandbox_pb2.StopRequest(id=sandbox_id)
164
+ self.stub.StopSandbox(request)
165
+
166
+ def exec_command(self, sandbox_id: str, command: List[str]) -> sandbox_pb2.ExecResponse:
167
+ """
168
+ Execute a command in a running sandbox.
169
+
170
+ Args:
171
+ sandbox_id: The ID of the sandbox.
172
+ command: The command to execute.
173
+
174
+ Returns:
175
+ The execution response containing stdout, stderr, and exit code.
176
+ """
177
+ request = sandbox_pb2.ExecRequest(id=sandbox_id, command=command)
178
+ return self.stub.ExecCommand(request)
179
+
180
+ def get_status(self, sandbox_id: str) -> str:
181
+ """
182
+ Get the status of a sandbox.
183
+
184
+ Args:
185
+ sandbox_id: The ID of the sandbox.
186
+
187
+ Returns:
188
+ The status of the sandbox (e.g., "running", "stopped").
189
+ """
190
+ request = sandbox_pb2.StatusRequest(id=sandbox_id)
191
+ response = self.stub.GetStatus(request)
192
+ return response.status
193
+
194
+ def snapshot_sandbox(self, sandbox_id: str) -> dict:
195
+ """
196
+ Snapshot a running sandbox.
197
+
198
+ Args:
199
+ sandbox_id: The ID of the sandbox to snapshot.
200
+
201
+ Returns:
202
+ A dictionary containing the paths to the snapshot file and memory file.
203
+ """
204
+ request = sandbox_pb2.SnapshotRequest(id=sandbox_id)
205
+ response = self.stub.SnapshotSandbox(request)
206
+ return {
207
+ "snapshot_path": response.snapshot_path,
208
+ "mem_file_path": response.mem_file_path,
209
+ }
210
+
211
+ def resume_sandbox(self, sandbox_id: str) -> None:
212
+ """
213
+ Resume a paused or snapshotted sandbox.
214
+
215
+ Args:
216
+ sandbox_id: The ID of the sandbox to resume.
217
+ """
218
+ request = sandbox_pb2.ResumeRequest(id=sandbox_id)
219
+ self.stub.ResumeSandbox(request)
220
+