deepagents-microsandbox 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,21 @@
1
+ """Microsandbox integration for DeepAgents.
2
+
3
+ This package provides:
4
+ - MicrosandboxBackend: Sandbox backend for executing commands in Microsandbox containers
5
+ - MicrosandboxProvider: Provider for managing Microsandbox lifecycle
6
+
7
+ Example usage:
8
+ ```python
9
+ from deepagents_microsandbox import MicrosandboxBackend, MicrosandboxProvider
10
+
11
+ provider = MicrosandboxProvider(server_url="http://localhost:5555")
12
+ sandbox = provider.get_or_create(sandbox_id=None)
13
+ result = sandbox.execute("echo 'Hello'")
14
+ provider.delete(sandbox_id=sandbox.id)
15
+ ```
16
+ """
17
+
18
+ from deepagents_microsandbox.backend import MicrosandboxBackend
19
+ from deepagents_microsandbox.provider import MicrosandboxProvider
20
+
21
+ __all__ = ["MicrosandboxBackend", "MicrosandboxProvider"]
@@ -0,0 +1,250 @@
1
+ """Microsandbox backend for DeepAgents.
2
+
3
+ This module provides MicrosandboxBackend, a sandbox backend implementation
4
+ that executes commands in Microsandbox containers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import asyncio
9
+ import base64
10
+ from contextlib import asynccontextmanager
11
+ from typing import TYPE_CHECKING, cast
12
+
13
+ import aiohttp
14
+ from deepagents.backends.protocol import ExecuteResponse, FileDownloadResponse, FileOperationError, FileUploadResponse
15
+ from deepagents.backends.sandbox import BaseSandbox
16
+
17
+ if TYPE_CHECKING:
18
+ from microsandbox import PythonSandbox
19
+
20
+
21
+ class MicrosandboxBackend(BaseSandbox):
22
+ """Microsandbox backend for DeepAgents.
23
+
24
+ This backend executes commands in Microsandbox containers via the
25
+ Microsandbox SDK. It inherits from BaseSandbox, which provides
26
+ implementations for read, write, edit, grep, and glob operations
27
+ using shell commands executed via execute().
28
+
29
+ The Microsandbox SDK is async-only, so this backend uses asyncio.run()
30
+ to bridge sync execute() calls to the async API. For async callers,
31
+ use aexecute() directly for better performance.
32
+
33
+ Example:
34
+ ```python
35
+ from microsandbox import PythonSandbox
36
+ from deepagents_microsandbox import MicrosandboxBackend
37
+
38
+
39
+ async def main():
40
+ async with PythonSandbox.create(name="my-sandbox") as sandbox:
41
+ backend = MicrosandboxBackend(sandbox, sandbox.name)
42
+ result = backend.execute("echo 'Hello, World!'")
43
+ print(result.output)
44
+
45
+
46
+ asyncio.run(main())
47
+ ```
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ sandbox: PythonSandbox,
53
+ sandbox_id: str,
54
+ *,
55
+ timeout: int = 30 * 60,
56
+ ) -> None:
57
+ """Initialize the Microsandbox backend.
58
+
59
+ Args:
60
+ sandbox: An active Microsandbox PythonSandbox instance.
61
+ Must already be started (inside async context manager or
62
+ after calling start()).
63
+ sandbox_id: Unique identifier for this sandbox instance.
64
+ Typically, in the format "namespace/name".
65
+ timeout: Command execution timeout in seconds. Default: 30 minutes.
66
+ """
67
+ self._sandbox = sandbox
68
+ self._sandbox_id = sandbox_id
69
+ self._timeout = timeout
70
+
71
+ @property
72
+ def id(self) -> str:
73
+ """Return the unique identifier for this sandbox."""
74
+ return self._sandbox_id
75
+
76
+ def execute(self, command: str) -> ExecuteResponse:
77
+ """Execute a command synchronously.
78
+
79
+ This method bridges to the async Microsandbox SDK by using
80
+ asyncio.run(). For async callers, use aexecute() instead.
81
+
82
+ Args:
83
+ command: Shell command to execute.
84
+
85
+ Returns:
86
+ ExecuteResponse with combined stdout/stderr output and exit code.
87
+ """
88
+ return asyncio.run(self.aexecute(command))
89
+
90
+ async def aexecute(self, command: str) -> ExecuteResponse:
91
+ """Execute a command asynchronously.
92
+
93
+ This is the native async implementation that directly calls
94
+ the Microsandbox SDK.
95
+
96
+ Args:
97
+ command: Shell command to execute.
98
+
99
+ Returns:
100
+ ExecuteResponse with combined stdout/stderr output and exit code.
101
+ """
102
+ async with self._ensure_session():
103
+ result = await self._sandbox.command.run(
104
+ command="bash",
105
+ args=["-c", command],
106
+ timeout=self._timeout,
107
+ )
108
+
109
+ # Get stdout and stderr
110
+ stdout = await result.output()
111
+ stderr = await result.error()
112
+
113
+ # Combine stdout and stderr (stderr appended if non-empty)
114
+ output = stdout
115
+ if stderr:
116
+ output += "\n" if not output.endswith("\n") else ""
117
+ output += stderr
118
+
119
+ # Truncate output if it exceeds limits (default 10MB) to prevent memory issues
120
+ # The underlying JSON-RPC still transfers everything, but we protect the agent here.
121
+ max_output_length = 10 * 1024 * 1024 # 10MB
122
+ truncated = False
123
+ if len(output) > max_output_length:
124
+ output = output[:max_output_length] + "\n... [Output truncated]"
125
+ truncated = True
126
+
127
+ return ExecuteResponse(
128
+ output=output,
129
+ exit_code=result.exit_code,
130
+ # ExecuteResponse protocol doesn't strictly define 'truncated' yet but it's good practice
131
+ # and might be supported in future versions or custom implementations.
132
+ # We ignore mypy check here if the attribute is missing in the installed version.
133
+ # truncated=truncated, # type: ignore
134
+ )
135
+
136
+ @asynccontextmanager
137
+ async def _ensure_session(self):
138
+ """Ensure the sandbox has a valid session for the current loop.
139
+
140
+ If the sandbox's session is closed or belongs to a different loop
141
+ (which happens when bridging sync calls via asyncio.run), this
142
+ creates a temporary session for the duration of the context.
143
+ """
144
+ session = getattr(self._sandbox, "_session", None)
145
+ try:
146
+ current_loop = asyncio.get_running_loop()
147
+ except RuntimeError:
148
+ current_loop = None
149
+
150
+ # Check if existing session is valid for this loop
151
+ if (
152
+ session is not None
153
+ and not session.closed
154
+ and getattr(session, "_loop", None) is current_loop
155
+ ):
156
+ yield
157
+ return
158
+
159
+ # Create a temporary session
160
+ async with aiohttp.ClientSession() as new_session:
161
+ old_session = session
162
+ # We have to access the private attribute to inject the session
163
+ # because the SDK doesn't expose a way to swap it.
164
+ self._sandbox._session = new_session # noqa: SLF001
165
+ try:
166
+ yield
167
+ finally:
168
+ self._sandbox._session = old_session # noqa: SLF001
169
+
170
+ def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
171
+ """Upload files to the sandbox via base64-encoded shell commands.
172
+
173
+ Microsandbox doesn't have a native file upload API, so we use
174
+ shell commands with base64 encoding to transfer file contents.
175
+ Content is split into chunks to avoid ARG_MAX limits.
176
+
177
+ Args:
178
+ files: List of (path, content) tuples to upload.
179
+
180
+ Returns:
181
+ List of FileUploadResponse objects, one per input file.
182
+ """
183
+ return asyncio.run(self._aupload_files(files))
184
+
185
+ async def _aupload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
186
+ return [await self._aupload_file(path, content) for path, content in files]
187
+
188
+ async def _aupload_file(self, file_path: str, content: bytes) -> FileUploadResponse:
189
+ """Upload a single file using chunked writes."""
190
+ # 1. Create parent directory
191
+ # 2. Truncate/create the file
192
+ cmd_init = f"mkdir -p \"$(dirname '{file_path}')\" && : > '{file_path}'"
193
+ result = await self.aexecute(cmd_init)
194
+ if result.exit_code != 0:
195
+ return FileUploadResponse(path=file_path, error="permission_denied") # Generic error
196
+
197
+ if not content:
198
+ return FileUploadResponse(path=file_path, error=None)
199
+
200
+ # 3. Append chunks avoiding ARG_MAX
201
+ # Typical ARG_MAX is ~128KB - 2MB. usage of 50KB chunks is safe.
202
+ chunk_size = 50 * 1024
203
+
204
+ for i in range(0, len(content), chunk_size):
205
+ chunk = content[i : i + chunk_size]
206
+ chunk_b64 = base64.b64encode(chunk).decode("ascii")
207
+
208
+ # append chunk to file
209
+ cmd_append = f"echo -n '{chunk_b64}' | base64 -d >> '{file_path}'"
210
+ result = await self.aexecute(cmd_append)
211
+ if result.exit_code != 0:
212
+ return FileUploadResponse(path=file_path, error="permission_denied")
213
+
214
+ return FileUploadResponse(path=file_path, error=None)
215
+
216
+ def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
217
+ """Download files from the sandbox via base64-encoded shell commands.
218
+
219
+ Microsandbox doesn't have a native file download API, so we use
220
+ shell commands with base64 encoding to transfer file contents.
221
+
222
+ Args:
223
+ paths: List of file paths to download.
224
+
225
+ Returns:
226
+ List of FileDownloadResponse objects, one per input path.
227
+ """
228
+ return asyncio.run(self._adownload_files(paths))
229
+
230
+ async def _adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
231
+ return [await self._adownload_file(path) for path in paths]
232
+
233
+ async def _adownload_file(self, file_path: str) -> FileDownloadResponse:
234
+ """Download a single file."""
235
+ cmd = (
236
+ f"[ -d '{file_path}' ] && echo '__IS_DIRECTORY__' && exit 1; "
237
+ f"[ ! -f '{file_path}' ] && echo '__FILE_NOT_FOUND__' && exit 1; "
238
+ f"base64 '{file_path}'"
239
+ )
240
+ result = await self.aexecute(cmd)
241
+
242
+ if result.exit_code == 0:
243
+ content = base64.b64decode(result.output.strip())
244
+ return FileDownloadResponse(path=file_path, content=content, error=None)
245
+
246
+ output = result.output.strip()
247
+ errors = {"__IS_DIRECTORY__": "is_directory", "__FILE_NOT_FOUND__": "file_not_found"}
248
+ error = cast("FileOperationError", errors.get(output, "permission_denied"))
249
+ return FileDownloadResponse(path=file_path, content=None, error=error)
250
+
@@ -0,0 +1,356 @@
1
+ """Microsandbox provider for DeepAgents.
2
+
3
+ This module provides MicrosandboxProvider, a sandbox provider that manages
4
+ the lifecycle of Microsandbox containers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+ import asyncio
9
+ import os
10
+ import uuid
11
+ from typing import Any
12
+
13
+ import aiohttp
14
+ from deepagents.backends.sandbox import SandboxInfo, SandboxListResponse, SandboxProvider
15
+ from microsandbox import PythonSandbox
16
+
17
+ from deepagents_microsandbox.backend import MicrosandboxBackend
18
+
19
+
20
+ class MicrosandboxProvider(SandboxProvider[dict[str, Any]]):
21
+ """Microsandbox provider for managing sandbox lifecycle.
22
+
23
+ This provider creates, connects to, and deletes Microsandbox containers.
24
+ It integrates with the DeepAgents framework via the SandboxProvider interface.
25
+
26
+ The provider manages aiohttp.ClientSession instances manually rather than
27
+ using the async context manager pattern, since DeepAgents expects persistent
28
+ sandbox instances.
29
+
30
+ Sandbox IDs are in the format "namespace/name".
31
+
32
+ Example:
33
+ ```python
34
+ from deepagents_microsandbox import MicrosandboxProvider
35
+
36
+ provider = MicrosandboxProvider(server_url="http://localhost:5555", namespace="my-project")
37
+
38
+ # Create a new sandbox
39
+ sandbox = provider.get_or_create(
40
+ sandbox_id=None,
41
+ image="microsandbox/python",
42
+ memory=1024,
43
+ )
44
+
45
+ # Execute commands
46
+ result = sandbox.execute("python --version")
47
+
48
+ # Clean up
49
+ provider.delete(sandbox_id=sandbox.id)
50
+ ```
51
+ """
52
+
53
+ def __init__(
54
+ self,
55
+ server_url: str | None = None,
56
+ api_key: str | None = None,
57
+ namespace: str = "default",
58
+ ) -> None:
59
+ """Initialize the Microsandbox provider.
60
+
61
+ Args:
62
+ server_url: Microsandbox server URL. Defaults to MSB_SERVER_URL
63
+ environment variable or "http://127.0.0.1:5555".
64
+ api_key: API key for authentication. Defaults to MSB_API_KEY
65
+ environment variable.
66
+ namespace: Namespace for organizing sandboxes. Default: "default".
67
+ """
68
+ self._server_url = server_url or os.environ.get("MSB_SERVER_URL", "http://127.0.0.1:5555")
69
+ self._api_key = api_key or os.environ.get("MSB_API_KEY")
70
+ self._namespace = namespace
71
+
72
+ # Track active sandboxes and their sessions for cleanup
73
+ self._active_sandboxes: dict[str, tuple[PythonSandbox, aiohttp.ClientSession]] = {}
74
+
75
+ # Shared session for API calls (alist)
76
+ self._shared_session: aiohttp.ClientSession | None = None
77
+
78
+ async def close(self) -> None:
79
+ """Close the provider and release resources."""
80
+ if self._shared_session:
81
+ await self._shared_session.close()
82
+ self._shared_session = None
83
+
84
+ # Note: We don't automatically close active sandboxes here as they might be
85
+ # intended to persist. Use delete() for that.
86
+
87
+ def list(
88
+ self,
89
+ *,
90
+ cursor: str | None = None,
91
+ **kwargs: Any, # noqa: ANN401
92
+ ) -> SandboxListResponse[dict[str, Any]]:
93
+ """Async list of sandboxes via the sandbox.metrics.get JSON-RPC endpoint.
94
+
95
+ Args:
96
+ cursor: Ignored (the API does not support pagination).
97
+ **kwargs: Optional provider-specific filters:
98
+ namespace: Namespace to query. Defaults to ``"*"`` (all).
99
+
100
+ Returns:
101
+ SandboxListResponse with sandbox info items and cursor=None.
102
+ """
103
+ return asyncio.run(self.alist(cursor=cursor, **kwargs))
104
+
105
+ async def alist(
106
+ self,
107
+ *,
108
+ cursor: str | None = None,
109
+ **kwargs: Any, # noqa: ANN401
110
+ ) -> SandboxListResponse[dict[str, Any]]:
111
+ """Async list of sandboxes via the sandbox.metrics.get JSON-RPC endpoint.
112
+
113
+ Args:
114
+ cursor: Ignored (the API does not support pagination).
115
+ **kwargs: Optional provider-specific filters:
116
+ namespace: Namespace to query. Defaults to ``"*"`` (all).
117
+
118
+ Returns:
119
+ SandboxListResponse with sandbox info items and cursor=None.
120
+ """
121
+ namespace = kwargs.get("namespace", "*")
122
+
123
+ payload = {
124
+ "jsonrpc": "2.0",
125
+ "method": "sandbox.metrics.get",
126
+ "params": {"namespace": namespace},
127
+ "id": str(uuid.uuid4()),
128
+ }
129
+
130
+ headers: dict[str, str] = {"Content-Type": "application/json"}
131
+ if self._api_key:
132
+ headers["Authorization"] = f"Bearer {self._api_key}"
133
+
134
+ current_loop = asyncio.get_running_loop()
135
+ if (
136
+ self._shared_session is None
137
+ or self._shared_session.closed
138
+ or getattr(self._shared_session, "_loop", None) is not current_loop
139
+ ):
140
+ self._shared_session = aiohttp.ClientSession()
141
+
142
+ async with self._shared_session.post(
143
+ f"{self._server_url}/api/v1/rpc",
144
+ json=payload,
145
+ headers=headers,
146
+ ) as resp:
147
+ resp.raise_for_status()
148
+ data = await resp.json()
149
+
150
+ result = data.get("result", {})
151
+ sandboxes = result.get("sandboxes", [])
152
+
153
+ items: list[SandboxInfo[dict[str, Any]]] = []
154
+ for entry in sandboxes:
155
+ ns = entry.get("namespace", self._namespace)
156
+ name = entry.get("name", "")
157
+ metadata = {k: v for k, v in entry.items() if k not in ("namespace", "name")}
158
+ items.append(
159
+ SandboxInfo(
160
+ sandbox_id=f"{ns}/{name}",
161
+ metadata=metadata, # type: ignore[arg-type]
162
+ )
163
+ )
164
+
165
+ return SandboxListResponse(items=items, cursor=None)
166
+
167
+ def get_or_create(
168
+ self,
169
+ *,
170
+ sandbox_id: str | None = None,
171
+ timeout: int = 180,
172
+ image: str | None = None,
173
+ memory: int = 512,
174
+ cpus: float = 1.0,
175
+ **kwargs: Any, # noqa: ANN401, ARG002
176
+ ) -> MicrosandboxBackend:
177
+ """Get an existing sandbox or create a new one.
178
+
179
+ Args:
180
+ sandbox_id: Unique identifier of an existing sandbox to retrieve.
181
+ Format: "namespace/name" or just "name" (uses default namespace).
182
+ If None, creates a new sandbox with an auto-generated name.
183
+ timeout: Sandbox startup timeout in seconds. Default: 180.
184
+ image: Docker image to use. Default: Microsandbox Python image.
185
+ memory: Memory limit in MB. Default: 512.
186
+ cpus: CPU limit. Default: 1.0.
187
+ **kwargs: Additional arguments (ignored).
188
+
189
+ Returns:
190
+ MicrosandboxBackend instance connected to the sandbox.
191
+
192
+ Raises:
193
+ RuntimeError: If connection to an existing sandbox fails.
194
+ """
195
+ return asyncio.run(
196
+ self.aget_or_create(
197
+ sandbox_id=sandbox_id,
198
+ timeout=timeout,
199
+ image=image,
200
+ memory=memory,
201
+ cpus=cpus,
202
+ )
203
+ )
204
+
205
+ async def aget_or_create(
206
+ self,
207
+ *,
208
+ sandbox_id: str | None = None,
209
+ timeout: int = 180,
210
+ image: str | None = None,
211
+ memory: int = 512,
212
+ cpus: float = 1.0,
213
+ **kwargs: Any, # noqa: ANN401, ARG002
214
+ ) -> MicrosandboxBackend:
215
+ """Async version of get_or_create.
216
+
217
+ Args:
218
+ sandbox_id: Unique identifier of an existing sandbox to retrieve.
219
+ timeout: Sandbox startup timeout in seconds. Default: 180.
220
+ image: Docker image to use. Default: Microsandbox Python image.
221
+ memory: Memory limit in MB. Default: 512.
222
+ cpus: CPU limit. Default: 1.0.
223
+ **kwargs: Additional arguments (ignored).
224
+
225
+ Returns:
226
+ MicrosandboxBackend instance connected to the sandbox.
227
+ """
228
+ # Parse sandbox_id to extract namespace and name
229
+ if sandbox_id is not None:
230
+ if "/" in sandbox_id:
231
+ namespace, name = sandbox_id.split("/", 1)
232
+ else:
233
+ namespace = self._namespace
234
+ name = sandbox_id
235
+ else:
236
+ namespace = self._namespace
237
+ name = None # Auto-generate
238
+
239
+ # Check if we already have this sandbox active
240
+ full_id = f"{namespace}/{name}" if name else None
241
+ if full_id and full_id in self._active_sandboxes:
242
+ sandbox, _ = self._active_sandboxes[full_id]
243
+ return MicrosandboxBackend(sandbox, full_id)
244
+
245
+ # Create aiohttp session manually (not using context manager)
246
+ session = aiohttp.ClientSession()
247
+
248
+ try:
249
+ # Create PythonSandbox instance
250
+ sandbox = PythonSandbox(
251
+ server_url=self._server_url,
252
+ namespace=namespace,
253
+ name=name,
254
+ api_key=self._api_key,
255
+ )
256
+
257
+ # Inject our session
258
+ sandbox._session = session # noqa: SLF001
259
+
260
+ # Start the sandbox
261
+ start_kwargs: dict[str, Any] = {
262
+ "timeout": float(timeout),
263
+ "memory": memory,
264
+ "cpus": cpus,
265
+ }
266
+ if image is not None:
267
+ start_kwargs["image"] = image
268
+
269
+ await sandbox.start(**start_kwargs)
270
+
271
+ # Get the actual sandbox ID (name may have been auto-generated)
272
+ actual_name = sandbox._name # noqa: SLF001
273
+ full_sandbox_id = f"{namespace}/{actual_name}"
274
+
275
+ # Track the sandbox
276
+ self._active_sandboxes[full_sandbox_id] = (sandbox, session)
277
+
278
+ return MicrosandboxBackend(sandbox, full_sandbox_id)
279
+
280
+ except Exception:
281
+ # Clean up session on failure
282
+ await session.close()
283
+ raise
284
+
285
+ def delete(
286
+ self,
287
+ *,
288
+ sandbox_id: str,
289
+ **kwargs: Any, # noqa: ANN401, ARG002
290
+ ) -> None:
291
+ """Delete a sandbox instance.
292
+
293
+ This method is idempotent - calling delete on a non-existent or
294
+ already-deleted sandbox will succeed without raising an error.
295
+
296
+ Args:
297
+ sandbox_id: Unique identifier of the sandbox to delete.
298
+ Format: "namespace/name".
299
+ **kwargs: Additional arguments (ignored).
300
+ """
301
+ asyncio.run(self.adelete(sandbox_id=sandbox_id))
302
+
303
+ async def adelete(
304
+ self,
305
+ *,
306
+ sandbox_id: str,
307
+ **kwargs: Any, # noqa: ANN401, ARG002
308
+ ) -> None:
309
+ """Async version of delete.
310
+
311
+ Args:
312
+ sandbox_id: Unique identifier of the sandbox to delete.
313
+ **kwargs: Additional arguments (ignored).
314
+ """
315
+ # Normalize sandbox_id
316
+ if "/" not in sandbox_id:
317
+ sandbox_id = f"{self._namespace}/{sandbox_id}"
318
+
319
+ # Check if we have this sandbox tracked
320
+ if sandbox_id not in self._active_sandboxes:
321
+ # Sandbox not tracked - either never created or already deleted
322
+ # Idempotent: don't raise an error
323
+ return
324
+
325
+ sandbox, session = self._active_sandboxes.pop(sandbox_id)
326
+
327
+ current_loop = asyncio.get_running_loop()
328
+
329
+ try:
330
+ # If session is invalid for current loop, use a temporary one for stopping
331
+ if (
332
+ session.closed
333
+ or getattr(session, "_loop", None) is not current_loop
334
+ ):
335
+ async with aiohttp.ClientSession() as temp_session:
336
+ # We need to temporarily set the session on the sandbox to stop it
337
+ original_session = getattr(sandbox, "_session", None)
338
+ sandbox._session = temp_session
339
+ try:
340
+ await sandbox.stop()
341
+ finally:
342
+ sandbox._session = original_session
343
+ else:
344
+ # Session is valid, just stop
345
+ await sandbox.stop()
346
+
347
+ except Exception: # noqa: BLE001, S110, S112
348
+ # Ignore errors during stop (sandbox might already be stopped)
349
+ pass
350
+ finally:
351
+ # Only close the session if it belongs to the current loop
352
+ if (
353
+ not session.closed
354
+ and getattr(session, "_loop", None) is current_loop
355
+ ):
356
+ await session.close()
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepagents-microsandbox
3
+ Version: 1.0.0
4
+ Summary: Microsandbox backend for DeepAgents
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: deepagents>=0.3.10
8
+ Requires-Dist: microsandbox>=0.1.8
9
+ Description-Content-Type: text/markdown
10
+
11
+ # deepagents-microsandbox
12
+
13
+ Microsandbox backend for the [DeepAgents](https://github.com/shkarupa-alex/deepagents) framework. This package enables DeepAgents to launch and control isolated sandbox environments using [Microsandbox](https://github.com/shkarupa-alex/microsandbox).
14
+
15
+ ## Key Features
16
+
17
+ - **Sandbox Provider**: Manages the lifecycle of microsandbox instances (create, list, delete).
18
+ - **Sandbox Backend**: Provides a standard interface for file operations and command execution within a sandbox.
19
+ - **DeepAgents Integration**: Fully compatible with the DeepAgents `SandboxProvider` and `SandboxBackend` interfaces.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install deepagents-microsandbox
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```python
30
+ from deepagents_microsandbox import MicrosandboxProvider
31
+
32
+ # Initialize the provider connecting to a microsandbox server
33
+ provider = MicrosandboxProvider(server_url="http://localhost:5555")
34
+
35
+ # Create or get a sandbox
36
+ sandbox = provider.get_or_create(sandbox_id=None)
37
+
38
+ # Execute a command
39
+ result = sandbox.execute("echo 'Hello World'")
40
+ print(result.stdout)
41
+
42
+ # Clean up
43
+ provider.delete(sandbox_id=sandbox.id)
44
+ ```
45
+
46
+ ## Development
47
+
48
+ This project uses `uv` for dependency management.
49
+
50
+ ### Prerequisites
51
+
52
+ - [uv](https://github.com/astral-sh/uv) installed.
53
+ - Python 3.11 or higher.
54
+
55
+ ### Setup
56
+
57
+ Install dependencies:
58
+
59
+ ```bash
60
+ uv sync
61
+ ```
62
+
63
+ ### Testing
64
+
65
+ Tests are split into unit tests (mocked) and integration tests (requiring a real server).
66
+
67
+ **Run Unit Tests (Mocked)**
68
+ These tests mock the HTTP interactions and do not require a running server.
69
+ ```bash
70
+ uv run pytest
71
+ ```
72
+
73
+ **Run Integration Tests**
74
+ These tests verify behavior against a real microsandbox server.
75
+ 1. Start a microsandbox server in dev mode:
76
+ ```bash
77
+ msb server start --dev
78
+ ```
79
+ 2. Run the tests with the environment variable:
80
+ ```bash
81
+ MICROSANDBOX_DEV_SERVER=http://127.0.0.1:5555 uv run pytest
82
+ ```
@@ -0,0 +1,7 @@
1
+ deepagents_microsandbox/__init__.py,sha256=8MCy9kr-EqB-sxq4uvcPeLYVkdh683csCHK9J6xANZo,755
2
+ deepagents_microsandbox/backend.py,sha256=Pq-Byw5saaSiIONPGZ79pzLdxqZWWhhEgLFdheT-LMM,9489
3
+ deepagents_microsandbox/provider.py,sha256=M0hG4aUwSakgYvkKpcBZWuVMR-JvguFKjbrBYIyhKd8,12305
4
+ deepagents_microsandbox-1.0.0.dist-info/METADATA,sha256=_fV8jFwH4_H0b2UTU1JGy09CDjoSb9JewoISb_3KWWA,2148
5
+ deepagents_microsandbox-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
6
+ deepagents_microsandbox-1.0.0.dist-info/licenses/LICENSE,sha256=3PvsVOKxa67dur4MbUpUsjPIHomzUPIZ2fJ72Ntzkz4,1070
7
+ deepagents_microsandbox-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shkarupa Alex
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.