openhands-workspace 1.0.0__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.
@@ -0,0 +1,10 @@
1
+ """OpenHands Workspace - Docker and container-based workspace implementations."""
2
+
3
+ from .docker import DockerWorkspace
4
+ from .remote_api import APIRemoteWorkspace
5
+
6
+
7
+ __all__ = [
8
+ "DockerWorkspace",
9
+ "APIRemoteWorkspace",
10
+ ]
@@ -0,0 +1,6 @@
1
+ """Docker workspace implementation."""
2
+
3
+ from .workspace import DockerWorkspace
4
+
5
+
6
+ __all__ = ["DockerWorkspace"]
@@ -0,0 +1,339 @@
1
+ """Docker-based remote workspace implementation."""
2
+
3
+ import os
4
+ import subprocess
5
+ import sys
6
+ import threading
7
+ import time
8
+ import uuid
9
+ from typing import Any
10
+ from urllib.request import urlopen
11
+
12
+ from pydantic import Field, PrivateAttr, model_validator
13
+
14
+ from openhands.agent_server.docker.build import (
15
+ BuildOptions,
16
+ PlatformType,
17
+ TargetType,
18
+ build,
19
+ )
20
+ from openhands.sdk.logger import get_logger
21
+ from openhands.sdk.utils.command import execute_command
22
+ from openhands.sdk.workspace import RemoteWorkspace
23
+
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ def check_port_available(port: int) -> bool:
29
+ """Check if a port is available for binding."""
30
+ import socket
31
+
32
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
33
+ try:
34
+ sock.bind(("0.0.0.0", port))
35
+ return True
36
+ except OSError:
37
+ time.sleep(0.1)
38
+ return False
39
+ finally:
40
+ sock.close()
41
+
42
+
43
+ def find_available_tcp_port(
44
+ min_port: int = 30000, max_port: int = 39999, max_attempts: int = 50
45
+ ) -> int:
46
+ """Find an available TCP port in a specified range."""
47
+ import random
48
+
49
+ rng = random.SystemRandom()
50
+ ports = list(range(min_port, max_port + 1))
51
+ rng.shuffle(ports)
52
+
53
+ for port in ports[:max_attempts]:
54
+ if check_port_available(port):
55
+ return port
56
+ return -1
57
+
58
+
59
+ class DockerWorkspace(RemoteWorkspace):
60
+ """Remote workspace that sets up and manages a Docker container.
61
+
62
+ This workspace creates a Docker container running the OpenHands agent server,
63
+ waits for it to become healthy, and then provides remote workspace operations
64
+ through the container's HTTP API.
65
+
66
+ Example:
67
+ with DockerWorkspace(base_image="python:3.12") as workspace:
68
+ result = workspace.execute_command("ls -la")
69
+ """
70
+
71
+ # Override parent fields with defaults
72
+ working_dir: str = Field(
73
+ default="/workspace",
74
+ description="Working directory inside the container.",
75
+ )
76
+ host: str = Field(
77
+ default="",
78
+ description=("Remote host URL (set automatically during container startup)."),
79
+ )
80
+
81
+ # Docker-specific configuration
82
+ base_image: str | None = Field(
83
+ default=None,
84
+ description="Base Docker image to use for the agent server container. "
85
+ "Mutually exclusive with server_image.",
86
+ )
87
+ server_image: str | None = Field(
88
+ default=None,
89
+ description=(
90
+ "Pre-built agent server image to use. If None, builds from base_image."
91
+ "Mutually exclusive with base_image."
92
+ ),
93
+ )
94
+ host_port: int | None = Field(
95
+ default=None,
96
+ description="Port to bind the container to. If None, finds available port.",
97
+ )
98
+ forward_env: list[str] = Field(
99
+ default_factory=lambda: ["DEBUG"],
100
+ description="Environment variables to forward to the container.",
101
+ )
102
+ mount_dir: str | None = Field(
103
+ default=None,
104
+ description="Optional host directory to mount into the container.",
105
+ )
106
+ detach_logs: bool = Field(
107
+ default=True, description="Whether to stream Docker logs in background."
108
+ )
109
+ target: TargetType = Field(
110
+ default="source", description="Build target for the Docker image."
111
+ )
112
+ platform: PlatformType = Field(
113
+ default="linux/amd64", description="Platform for the Docker image."
114
+ )
115
+ extra_ports: bool = Field(
116
+ default=False,
117
+ description="Whether to expose additional ports (VSCode, VNC).",
118
+ )
119
+
120
+ _container_id: str | None = PrivateAttr(default=None)
121
+ _logs_thread: threading.Thread | None = PrivateAttr(default=None)
122
+ _stop_logs: threading.Event = PrivateAttr(default_factory=threading.Event)
123
+ _image: str = PrivateAttr()
124
+
125
+ @model_validator(mode="after")
126
+ def _validate_images(self):
127
+ """Ensure exactly one of base_image or server_image is provided; cache it."""
128
+ if (self.base_image is None) == (self.server_image is None):
129
+ raise ValueError(
130
+ "Exactly one of 'base_image' or 'server_image' must be set."
131
+ )
132
+ return self
133
+
134
+ def model_post_init(self, context: Any) -> None:
135
+ """Set up the Docker container and initialize the remote workspace."""
136
+ # Determine port
137
+ if self.host_port is None:
138
+ self.host_port = find_available_tcp_port()
139
+ else:
140
+ self.host_port = int(self.host_port)
141
+
142
+ if not check_port_available(self.host_port):
143
+ raise RuntimeError(f"Port {self.host_port} is not available")
144
+
145
+ if self.extra_ports:
146
+ if not check_port_available(self.host_port + 1):
147
+ raise RuntimeError(
148
+ f"Port {self.host_port + 1} is not available for VSCode"
149
+ )
150
+ if not check_port_available(self.host_port + 2):
151
+ raise RuntimeError(
152
+ f"Port {self.host_port + 2} is not available for VNC"
153
+ )
154
+
155
+ # Ensure docker is available
156
+ docker_ver = execute_command(["docker", "version"]).returncode
157
+ if docker_ver != 0:
158
+ raise RuntimeError(
159
+ "Docker is not available. Please install and start "
160
+ "Docker Desktop/daemon."
161
+ )
162
+
163
+ # Build image if needed
164
+
165
+ if self.base_image:
166
+ if "ghcr.io/openhands/agent-server" in self.base_image:
167
+ raise RuntimeError(
168
+ "base_image cannot be a pre-built agent-server image. "
169
+ "Use server_image=... instead."
170
+ )
171
+ build_opts = BuildOptions(
172
+ base_image=self.base_image,
173
+ target=self.target,
174
+ platforms=[self.platform],
175
+ push=False,
176
+ )
177
+ tags = build(opts=build_opts)
178
+ assert tags and len(tags) > 0, "Build failed, no image tags returned"
179
+ self._image = tags[0]
180
+
181
+ elif self.server_image:
182
+ self._image = self.server_image
183
+ else:
184
+ raise RuntimeError("Unreachable: one of base_image or server_image is set")
185
+
186
+ # Prepare Docker run flags
187
+ flags: list[str] = []
188
+ for key in self.forward_env:
189
+ if key in os.environ:
190
+ flags += ["-e", f"{key}={os.environ[key]}"]
191
+
192
+ if self.mount_dir:
193
+ mount_path = "/workspace"
194
+ flags += ["-v", f"{self.mount_dir}:{mount_path}"]
195
+ logger.info(
196
+ "Mounting host dir %s to container path %s",
197
+ self.mount_dir,
198
+ mount_path,
199
+ )
200
+
201
+ ports = ["-p", f"{self.host_port}:8000"]
202
+ if self.extra_ports:
203
+ ports += [
204
+ "-p",
205
+ f"{self.host_port + 1}:8001", # VSCode
206
+ "-p",
207
+ f"{self.host_port + 2}:8002", # Desktop VNC
208
+ ]
209
+ flags += ports
210
+
211
+ # Run container
212
+ run_cmd = [
213
+ "docker",
214
+ "run",
215
+ "-d",
216
+ "--platform",
217
+ self.platform,
218
+ "--rm",
219
+ "--name",
220
+ f"agent-server-{uuid.uuid4()}",
221
+ *flags,
222
+ self._image,
223
+ "--host",
224
+ "0.0.0.0",
225
+ "--port",
226
+ "8000",
227
+ ]
228
+ proc = execute_command(run_cmd)
229
+ if proc.returncode != 0:
230
+ raise RuntimeError(f"Failed to run docker container: {proc.stderr}")
231
+
232
+ self._container_id = proc.stdout.strip()
233
+ logger.info("Started container: %s", self._container_id)
234
+
235
+ # Optionally stream logs in background
236
+ if self.detach_logs:
237
+ self._logs_thread = threading.Thread(
238
+ target=self._stream_docker_logs, daemon=True
239
+ )
240
+ self._logs_thread.start()
241
+
242
+ # Set host for RemoteWorkspace to use
243
+ # The container exposes port 8000, mapped to self.host_port
244
+ # Override parent's host initialization
245
+ object.__setattr__(self, "host", f"http://localhost:{self.host_port}")
246
+ object.__setattr__(self, "api_key", None)
247
+
248
+ # Wait for container to be healthy
249
+ self._wait_for_health()
250
+ logger.info("Docker workspace is ready at %s", self.host)
251
+
252
+ # Now initialize the parent RemoteWorkspace with the container URL
253
+ super().model_post_init(context)
254
+
255
+ def _stream_docker_logs(self) -> None:
256
+ """Stream Docker logs to stdout in the background."""
257
+ if not self._container_id:
258
+ return
259
+ try:
260
+ p = subprocess.Popen(
261
+ ["docker", "logs", "-f", self._container_id],
262
+ stdout=subprocess.PIPE,
263
+ stderr=subprocess.STDOUT,
264
+ text=True,
265
+ )
266
+ if p.stdout is None:
267
+ return
268
+ for line in iter(p.stdout.readline, ""):
269
+ if self._stop_logs.is_set():
270
+ break
271
+ if line:
272
+ sys.stdout.write(f"[DOCKER] {line}")
273
+ sys.stdout.flush()
274
+ except Exception as e:
275
+ sys.stderr.write(f"Error streaming docker logs: {e}\n")
276
+ finally:
277
+ try:
278
+ self._stop_logs.set()
279
+ except Exception:
280
+ pass
281
+
282
+ def _wait_for_health(self, timeout: float = 120.0) -> None:
283
+ """Wait for the Docker container to become healthy."""
284
+ start = time.time()
285
+ health_url = f"http://127.0.0.1:{self.host_port}/health"
286
+
287
+ while time.time() - start < timeout:
288
+ try:
289
+ with urlopen(health_url, timeout=1.0) as resp:
290
+ if 200 <= getattr(resp, "status", 200) < 300:
291
+ return
292
+ except Exception:
293
+ pass
294
+
295
+ # Check if container is still running
296
+ if self._container_id:
297
+ ps = execute_command(
298
+ [
299
+ "docker",
300
+ "inspect",
301
+ "-f",
302
+ "{{.State.Running}}",
303
+ self._container_id,
304
+ ]
305
+ )
306
+ if ps.stdout.strip() != "true":
307
+ logs = execute_command(["docker", "logs", self._container_id])
308
+ msg = (
309
+ "Container stopped unexpectedly. Logs:\n"
310
+ f"{logs.stdout}\n{logs.stderr}"
311
+ )
312
+ raise RuntimeError(msg)
313
+ time.sleep(1)
314
+ raise RuntimeError("Container failed to become healthy in time")
315
+
316
+ def __enter__(self) -> "DockerWorkspace":
317
+ """Context manager entry - returns the workspace itself."""
318
+ return self
319
+
320
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[no-untyped-def]
321
+ """Context manager exit - cleans up the Docker container."""
322
+ self.cleanup()
323
+
324
+ def __del__(self) -> None:
325
+ """Clean up the Docker container when the workspace is destroyed."""
326
+ self.cleanup()
327
+
328
+ def cleanup(self) -> None:
329
+ """Stop and remove the Docker container."""
330
+ if self._container_id:
331
+ # Stop logs streaming
332
+ self._stop_logs.set()
333
+ if self._logs_thread and self._logs_thread.is_alive():
334
+ self._logs_thread.join(timeout=2)
335
+
336
+ # Stop and remove the container
337
+ logger.info("Stopping container: %s", self._container_id)
338
+ execute_command(["docker", "stop", self._container_id])
339
+ self._container_id = None
File without changes
@@ -0,0 +1,6 @@
1
+ """Runtime API workspace implementation."""
2
+
3
+ from .workspace import APIRemoteWorkspace
4
+
5
+
6
+ __all__ = ["APIRemoteWorkspace"]
@@ -0,0 +1,296 @@
1
+ """API-based remote workspace implementation using runtime API."""
2
+
3
+ import uuid
4
+ from typing import Any
5
+ from urllib.request import urlopen
6
+
7
+ import httpx
8
+ import tenacity
9
+ from pydantic import Field, PrivateAttr
10
+
11
+ from openhands.sdk.logger import get_logger
12
+ from openhands.sdk.workspace.remote.base import RemoteWorkspace
13
+
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class APIRemoteWorkspace(RemoteWorkspace):
19
+ """Remote workspace using OpenHands runtime API.
20
+
21
+ Runtime API: https://runtime.all-hands.dev/
22
+
23
+ Example:
24
+ workspace = APIRemoteWorkspace(
25
+ runtime_api_url="https://runtime.eval.all-hands.dev",
26
+ runtime_api_key="your-api-key",
27
+ server_image="ghcr.io/openhands/agent-server:lastest-python",
28
+ )
29
+ """ # noqa: E501
30
+
31
+ # Parent fields
32
+ working_dir: str = Field(
33
+ default="/workspace",
34
+ description="Working directory inside the remote workspace",
35
+ )
36
+ host: str = Field(
37
+ default="undefined",
38
+ description="The remote host URL for the workspace."
39
+ " It will be set to the runtime URL after connecting.",
40
+ )
41
+
42
+ # Runtime API fields
43
+ runtime_api_url: str = Field(description="Base URL of the runtime API")
44
+ runtime_api_key: str = Field(description="API key for authentication")
45
+ server_image: str = Field(
46
+ description="Container image for the agent server. "
47
+ "It must be a public image or in a registry accessible by runtime API."
48
+ )
49
+ session_id: str | None = Field(
50
+ default_factory=lambda: f"agent-server-{uuid.uuid4()}",
51
+ description="Session ID (auto-generated if None)",
52
+ )
53
+ resource_factor: int = Field(
54
+ default=1, description="Resource scaling (1, 2, 4, or 8)"
55
+ )
56
+ runtime_class: str | None = Field(
57
+ default="sysbox", description="Runtime class (e.g., 'sysbox')"
58
+ )
59
+ init_timeout: float = Field(
60
+ default=300.0, description="Runtime init timeout (seconds)"
61
+ )
62
+ api_timeout: float = Field(
63
+ default=60.0, description="API request timeout (seconds)"
64
+ )
65
+ keep_alive: bool = Field(default=False, description="Keep runtime alive on cleanup")
66
+ pause_on_close: bool = Field(
67
+ default=False, description="Pause instead of stop on cleanup"
68
+ )
69
+
70
+ _runtime_id: str | None = PrivateAttr(default=None)
71
+ _runtime_url: str | None = PrivateAttr(default=None)
72
+ _session_api_key: str | None = PrivateAttr(default=None)
73
+
74
+ def model_post_init(self, context: Any) -> None:
75
+ """Set up the remote runtime and initialize the workspace."""
76
+ if self.resource_factor not in [1, 2, 4, 8]:
77
+ raise ValueError(
78
+ f"resource_factor must be 1, 2, 4, or 8, got {self.resource_factor}"
79
+ )
80
+
81
+ self.runtime_api_url = self.runtime_api_url.rstrip("/")
82
+
83
+ try:
84
+ self._start_or_attach_to_runtime()
85
+ super().model_post_init(context)
86
+ except Exception:
87
+ self.cleanup()
88
+ raise
89
+
90
+ def _start_or_attach_to_runtime(self) -> None:
91
+ """Start or attach to an existing runtime."""
92
+ if not self._check_existing_runtime():
93
+ self._start_runtime()
94
+
95
+ assert self._runtime_id and self._runtime_url, "Runtime ID/URL not set"
96
+ self._wait_until_runtime_alive()
97
+ logger.info(f"Runtime ready at {self._runtime_url}")
98
+ self.host = self._runtime_url.rstrip("/")
99
+ self.api_key = self._session_api_key
100
+
101
+ def _check_existing_runtime(self) -> bool:
102
+ """Check if there's an existing runtime for this session."""
103
+ try:
104
+ resp = self._send_api_request(
105
+ "GET", f"{self.runtime_api_url}/sessions/{self.session_id}"
106
+ )
107
+ data = resp.json()
108
+ status = data.get("status")
109
+ logger.info(f"Runtime status: {status}")
110
+
111
+ if status in ("running", "paused"):
112
+ self._parse_runtime_response(resp)
113
+ if status == "paused":
114
+ try:
115
+ self._resume_runtime()
116
+ except Exception as e:
117
+ logger.error(f"Resume failed: {e}")
118
+ return False
119
+ return True
120
+ return False
121
+ except httpx.HTTPStatusError as e:
122
+ if e.response.status_code == 404:
123
+ return False
124
+ raise
125
+
126
+ def _start_runtime(self) -> None:
127
+ """Start a new runtime."""
128
+ # For binary target, use the standalone binary
129
+ payload: dict[str, Any] = {
130
+ "image": self.server_image,
131
+ "command": "/usr/local/bin/openhands-agent-server --port 60000",
132
+ "working_dir": "/", # Match Dockerfile WORKDIR
133
+ "environment": {},
134
+ "session_id": self.session_id,
135
+ "run_as_user": 10001,
136
+ "fs_group": 10001,
137
+ # "environment": {"DEBUG": "true"},
138
+ }
139
+
140
+ if self.runtime_class:
141
+ payload["runtime_class"] = self.runtime_class
142
+ if self.resource_factor != 1:
143
+ payload["resource_factor"] = self.resource_factor
144
+
145
+ logger.info(f"Starting runtime with {self.server_image}")
146
+ logger.info(f"Payload: {payload}")
147
+ resp = self._send_api_request(
148
+ "POST",
149
+ f"{self.runtime_api_url}/start",
150
+ json=payload,
151
+ timeout=self.init_timeout,
152
+ )
153
+ self._parse_runtime_response(resp)
154
+ logger.info(f"Runtime {self._runtime_id} at {self._runtime_url}")
155
+
156
+ def _resume_runtime(self) -> None:
157
+ """Resume a paused runtime."""
158
+ resp = self._send_api_request(
159
+ "POST",
160
+ f"{self.runtime_api_url}/resume",
161
+ json={"runtime_id": self._runtime_id},
162
+ timeout=self.init_timeout,
163
+ )
164
+ self._parse_runtime_response(resp)
165
+
166
+ def _parse_runtime_response(self, response: httpx.Response) -> None:
167
+ """Parse the runtime response and extract connection info."""
168
+ data = response.json()
169
+ self._runtime_id = data.get("runtime_id") or data.get("id")
170
+ self._runtime_url = data.get("url")
171
+ self._session_api_key = data.get("session_api_key")
172
+ if not self._runtime_id or not self._runtime_url:
173
+ raise ValueError(f"Invalid runtime response: {data}")
174
+
175
+ @tenacity.retry(
176
+ stop=tenacity.stop_after_delay(300),
177
+ wait=tenacity.wait_exponential(multiplier=1, min=2, max=10),
178
+ retry=tenacity.retry_if_exception_type(RuntimeError),
179
+ reraise=True,
180
+ )
181
+ def _wait_until_runtime_alive(self) -> None:
182
+ """Wait until the runtime becomes alive and responsive."""
183
+ logger.info("Waiting for runtime to become alive...")
184
+
185
+ resp = self._send_api_request(
186
+ "GET", f"{self.runtime_api_url}/sessions/{self.session_id}"
187
+ )
188
+ data = resp.json()
189
+ pod_status = data.get("pod_status", "").lower()
190
+ logger.info(f"Pod status: {pod_status}")
191
+
192
+ # Log additional details for debugging
193
+ if pod_status == "pending":
194
+ container_statuses = data.get("container_statuses", [])
195
+ events = data.get("events", [])
196
+ if container_statuses:
197
+ logger.warning(f"Container statuses: {container_statuses}")
198
+ if events:
199
+ logger.warning(f"Pod events: {events}")
200
+ logger.debug(f"Full response: {data}")
201
+
202
+ restart_count = data.get("restart_count", 0)
203
+ if restart_count > 0:
204
+ restart_reasons = data.get("restart_reasons", [])
205
+ logger.warning(f"Pod restarts: {restart_count}, reasons: {restart_reasons}")
206
+
207
+ # Handle different pod states
208
+ if pod_status == "ready":
209
+ # Pod is ready, check health endpoint
210
+ health_url = f"{self._runtime_url}/health"
211
+ logger.info(f"Checking health at: {health_url}")
212
+ try:
213
+ with urlopen(health_url, timeout=5.0) as resp:
214
+ status = getattr(resp, "status", 200)
215
+ logger.info(f"Health check response: {status}")
216
+ if 200 <= status < 300:
217
+ logger.info("Runtime is alive!")
218
+ return
219
+ raise RuntimeError(f"Health check failed with status: {status}")
220
+ except Exception as e:
221
+ logger.warning(f"Health check failed: {e}")
222
+ raise RuntimeError(f"Runtime /health failed: {e}")
223
+ elif pod_status in ("not found", "pending", "running"):
224
+ # Transient states - continue retrying
225
+ logger.debug(f"Runtime not yet ready. Status: {pod_status}")
226
+ raise RuntimeError(f"Runtime not yet ready (status: {pod_status})")
227
+ elif pod_status in ("failed", "unknown", "crashloopbackoff"):
228
+ # Terminal failure states
229
+ pod_logs = data.get("pod_logs", "")
230
+ error_msg = f"Runtime failed (status: {pod_status})"
231
+ if pod_logs:
232
+ logger.error(f"Pod logs: {pod_logs}")
233
+ error_msg += f"\nPod logs: {pod_logs}"
234
+ if pod_status == "crashloopbackoff":
235
+ error_msg = (
236
+ "Runtime crashed and is restarting (possibly OOM). Try again."
237
+ )
238
+ raise ValueError(error_msg)
239
+ else:
240
+ # Unknown status - log and retry
241
+ logger.warning(f"Unknown pod status: {pod_status}, full response: {data}")
242
+ raise RuntimeError(f"Unknown pod status: {pod_status}")
243
+
244
+ def _send_api_request(self, method: str, url: str, **kwargs: Any) -> httpx.Response:
245
+ """Send an API request with error handling."""
246
+ logger.debug(f"Sending {method} request to {url}")
247
+ logger.debug(f"Client headers: {self._headers}")
248
+ response = self.client.request(method, url, **kwargs)
249
+ try:
250
+ response.raise_for_status()
251
+ except httpx.HTTPStatusError:
252
+ logger.debug(f"Request headers: {response.request.headers}")
253
+ try:
254
+ error_detail = response.json()
255
+ logger.info(f"API request failed: {error_detail}")
256
+ except Exception:
257
+ logger.info(f"API request failed: {response.text}")
258
+ raise
259
+ return response
260
+
261
+ def cleanup(self) -> None:
262
+ """Clean up the remote runtime."""
263
+ if not self._runtime_id:
264
+ return
265
+
266
+ try:
267
+ if self.keep_alive:
268
+ return
269
+
270
+ action = "pause" if self.pause_on_close else "stop"
271
+ logger.info(f"{action.capitalize()}ing runtime {self._runtime_id}")
272
+ self._send_api_request(
273
+ "POST",
274
+ f"{self.runtime_api_url}/{action}",
275
+ json={"runtime_id": self._runtime_id},
276
+ timeout=30.0,
277
+ )
278
+ except Exception as e:
279
+ logger.error(f"Cleanup error: {e}")
280
+ finally:
281
+ self._runtime_id = None
282
+ self._runtime_url = None
283
+ self._session_api_key = None
284
+ try:
285
+ self.client.close()
286
+ except Exception:
287
+ pass
288
+
289
+ def __del__(self) -> None:
290
+ self.cleanup()
291
+
292
+ def __enter__(self) -> "APIRemoteWorkspace":
293
+ return self
294
+
295
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
296
+ self.cleanup()
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: openhands-workspace
3
+ Version: 1.0.0
4
+ Summary: OpenHands Workspace - Docker and container-based workspace implementations
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: openhands-sdk
7
+ Requires-Dist: pydantic>=2.11.7
@@ -0,0 +1,10 @@
1
+ openhands/workspace/__init__.py,sha256=0UHu2lU1tjXhVcJSU6zQUaa3MLbqOLgGF8WdPSmjCPY,227
2
+ openhands/workspace/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ openhands/workspace/docker/__init__.py,sha256=axwSNLItKWxyxbZ1VuKKTrvnVAxALC9b2ZlTatnBJRE,111
4
+ openhands/workspace/docker/workspace.py,sha256=H1QiQqr_xig0Xcgc4_sDjeTng7XYGNj4aGPpfsS8dx8,11499
5
+ openhands/workspace/remote_api/__init__.py,sha256=RxDLFcveTGG-uLm-PInfxFnYwYPdehRmLf1r0_KTaP4,122
6
+ openhands/workspace/remote_api/workspace.py,sha256=zWXoq80wxIYA-BORl1ebqLzOTSEdJYan4QEqsk_YY0g,11363
7
+ openhands_workspace-1.0.0.dist-info/METADATA,sha256=PylvdehS6-WarDQNKn7SfmZZ6i0RnRq5F6nmTcwE0Fk,232
8
+ openhands_workspace-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ openhands_workspace-1.0.0.dist-info/top_level.txt,sha256=jHgVu9I0Blam8BXFgedoGKfglPF8XvW1TsJFIjcgP4E,10
10
+ openhands_workspace-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ openhands