langchain-backend-microsandbox 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.
- langchain_backend_microsandbox-0.1.0/LICENSE +22 -0
- langchain_backend_microsandbox-0.1.0/PKG-INFO +264 -0
- langchain_backend_microsandbox-0.1.0/README.md +247 -0
- langchain_backend_microsandbox-0.1.0/pyproject.toml +65 -0
- langchain_backend_microsandbox-0.1.0/src/langchain_backend_microsandbox/__init__.py +9 -0
- langchain_backend_microsandbox-0.1.0/src/langchain_backend_microsandbox/backend.py +471 -0
- langchain_backend_microsandbox-0.1.0/src/langchain_backend_microsandbox/py.typed +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LangChain
|
|
4
|
+
Copyright (c) 2026 dod-ia
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: langchain-backend-microsandbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Microsandbox backend integration for DeepAgents and LangChain.
|
|
5
|
+
Project-URL: Homepage, https://github.com/dod-ia/langchain-backend-microsandbox
|
|
6
|
+
Project-URL: Repository, https://github.com/dod-ia/langchain-backend-microsandbox
|
|
7
|
+
Project-URL: Issues, https://github.com/dod-ia/langchain-backend-microsandbox/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/dod-ia/langchain-backend-microsandbox/CHANGELOG.md
|
|
9
|
+
Author: dod-ia
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: deepagents,langchain,microsandbox,microvm,sandbox
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Requires-Dist: deepagents<0.8.0,>=0.7.35
|
|
15
|
+
Requires-Dist: microsandbox<0.8.0,>=0.7.14
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# langchain-backend-microsandbox
|
|
19
|
+
|
|
20
|
+
**A [Microsandbox](https://github.com/superradcompany/microsandbox) backend for [LangChain Deep Agents](https://github.com/langchain-ai/deepagents), bringing isolated microVM execution to autonomous AI agents.**
|
|
21
|
+
|
|
22
|
+
Run agent-generated commands and manage files inside lightweight, isolated virtual machines through Deep Agents' native BaseSandbox interface.
|
|
23
|
+
|
|
24
|
+
Built on Microsandbox, this integration provides a seamless bridge between Deep Agents and Microsandbox's asynchronous execution and filesystem APIs.
|
|
25
|
+
|
|
26
|
+
Inspired by `kenwoodjw/langchain-microsandbox`, this implementation extends the original approach with a focus on compatibility, predictable behavior, and closer adherence to the BaseSandbox contract.
|
|
27
|
+
|
|
28
|
+
## Why Microsandbox?
|
|
29
|
+
|
|
30
|
+
AI agents increasingly need to execute code, manipulate files, and interact with shell environments to accomplish complex tasks.
|
|
31
|
+
|
|
32
|
+
Running these operations directly on the host introduces unnecessary security risks.
|
|
33
|
+
|
|
34
|
+
Microsandbox addresses this challenge by providing isolated microVM environments in which agent workloads can execute independently of the host system.
|
|
35
|
+
|
|
36
|
+
langchain-backend-microsandbox brings this isolation model to LangChain Deep Agents, allowing developers to use Microsandbox as their agent execution backend without implementing their own sandbox integration.
|
|
37
|
+
|
|
38
|
+
**The result: a standardized Deep Agents backend with isolated execution and native filesystem support.**
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
| Feature | Description |
|
|
43
|
+
|---|---|
|
|
44
|
+
| **Deep Agents integration** | Implements the `BaseSandbox` interface. |
|
|
45
|
+
| **Contract compliance** | Designed to closely follow the `BaseSandbox` behavior and expectations. |
|
|
46
|
+
| **Isolated execution** | Runs agent-generated commands inside Microsandbox microVMs. |
|
|
47
|
+
| **Async & sync execution** | Supports both synchronous and asynchronous command execution. |
|
|
48
|
+
| **Native file transfers** | Binary-safe file uploads and downloads through Microsandbox's filesystem API. |
|
|
49
|
+
| **Filesystem operations** | Inherits `read`, `write`, `edit`, `delete`, `ls`, `grep`, and `glob` from `BaseSandbox`. |
|
|
50
|
+
| **Timeout management** | Configurable execution timeouts with standardized timeout exit codes. |
|
|
51
|
+
| **Error normalization** | Maps filesystem exceptions to Deep Agents-compatible error codes. |
|
|
52
|
+
| **Atomic file uploads** | Uses temporary files and filesystem rename operations to avoid exposing partially written files. |
|
|
53
|
+
|
|
54
|
+
## Architecture & Compatibility
|
|
55
|
+
|
|
56
|
+
MicrosandboxSandbox acts as an adapter between LangChain Deep Agents and an existing Microsandbox instance.
|
|
57
|
+
|
|
58
|
+
It implements the backend-specific operations while relying on BaseSandbox for the higher-level filesystem functionality.
|
|
59
|
+
|
|
60
|
+
### Inherited BaseSandbox operations
|
|
61
|
+
|
|
62
|
+
The following operations are inherited directly from LangChain Deep Agents:
|
|
63
|
+
|
|
64
|
+
* `read()`, `write()`, `edit()`, `delete()`
|
|
65
|
+
* `ls()`, `grep()`, `glob()`
|
|
66
|
+
|
|
67
|
+
This approach avoids reimplementing filesystem behavior and helps maintain consistency with other Deep Agents sandbox backends.
|
|
68
|
+
|
|
69
|
+
### BaseSandbox behavior
|
|
70
|
+
|
|
71
|
+
#### Grep directory exclusions
|
|
72
|
+
|
|
73
|
+
Directories listed in `grep_exclude_dirs` are excluded from recursive searches, regardless of their depth in the directory tree.
|
|
74
|
+
|
|
75
|
+
However, explicitly specifying an excluded directory as the search root overrides this exclusion.
|
|
76
|
+
|
|
77
|
+
In other words:
|
|
78
|
+
- Searching from a parent directory skips excluded directories.
|
|
79
|
+
- Explicitly targeting an excluded directory still searches its contents.
|
|
80
|
+
|
|
81
|
+
This allows common directories such as `.git` to be excluded from general searches without preventing developers or agents from inspecting them when needed.
|
|
82
|
+
|
|
83
|
+
Reference: [BaseSandbox.grep_exclude_dirs](https://reference.langchain.com/python/deepagents/backends/sandbox/BaseSandbox/grep_exclude_dirs)
|
|
84
|
+
|
|
85
|
+
#### Command execution timeouts
|
|
86
|
+
|
|
87
|
+
Timeout behavior follows the `SandboxBackendProtocol` conventions:
|
|
88
|
+
|
|
89
|
+
- `None` uses the default timeout configured when creating the adapter.
|
|
90
|
+
- `0` disables the execution timeout.
|
|
91
|
+
- Negative values are rejected with a `ValueError`.
|
|
92
|
+
- When a command exceeds its configured timeout, the backend returns exit code `124`.
|
|
93
|
+
|
|
94
|
+
Reference: [SandboxBackendProtocol.execute](https://reference.langchain.com/python/deepagents/backends/protocol/SandboxBackendProtocol/execute)
|
|
95
|
+
|
|
96
|
+
#### Python runtime requirement
|
|
97
|
+
|
|
98
|
+
The Microsandbox VM must have Python 3 installed and available in `PATH`.
|
|
99
|
+
|
|
100
|
+
This is a requirement of the inherited `BaseSandbox` filesystem operations, which rely on Python scripts executed inside the sandbox.
|
|
101
|
+
|
|
102
|
+
Having Python installed on the host machine is not sufficient: the interpreter must be available inside the Microsandbox VM.
|
|
103
|
+
|
|
104
|
+
Make sure your sandbox image includes a compatible Python installation before initializing the backend.
|
|
105
|
+
|
|
106
|
+
## Requirements
|
|
107
|
+
|
|
108
|
+
- Python >= 3.12
|
|
109
|
+
- [Deep Agents](https://github.com/langchain-ai/deepagents) >= 0.7.35, < 0.8.0
|
|
110
|
+
- [Microsandbox](https://github.com/superradcompany/microsandbox) >= 0.7.14, < 0.8.0
|
|
111
|
+
- A supported Microsandbox runtime environment.
|
|
112
|
+
|
|
113
|
+
Microsandbox requires hardware virtualization support:
|
|
114
|
+
- **Linux**: KVM
|
|
115
|
+
- **macOS**: Apple Silicon virtualization support
|
|
116
|
+
- **Windows**: Windows Hypervisor Platform (WHP)
|
|
117
|
+
|
|
118
|
+
On **debian/ubuntu system**, you can install KVM by installing:
|
|
119
|
+
|
|
120
|
+
`sudo apt install qemu-system libvirt-daemon-system`
|
|
121
|
+
|
|
122
|
+
## Installation from sources
|
|
123
|
+
|
|
124
|
+
Clone the repository and install the package:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
git clone https://github.com/dod-ia/langchain-backend-microsandbox.git
|
|
128
|
+
|
|
129
|
+
cd langchain-backend-microsandbox
|
|
130
|
+
|
|
131
|
+
uv sync
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Get started
|
|
135
|
+
|
|
136
|
+
The adapter wraps an existing Microsandbox instance and exposes it as a Deep Agents backend.
|
|
137
|
+
|
|
138
|
+
Since Microsandbox provides an asynchronous API, sandbox creation and adapter initialization must be performed asynchronously.
|
|
139
|
+
|
|
140
|
+
### Basic example
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from microsandbox import Sandbox
|
|
144
|
+
from langchain_backend_microsandbox import MicrosandboxSandbox
|
|
145
|
+
from deepagents import create_deep_agent
|
|
146
|
+
|
|
147
|
+
model = init_chat_model("YOUR_MODEL")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def main():
|
|
151
|
+
# Create a sandbox from MicroSandbox API
|
|
152
|
+
sandbox = await Sandbox.create(
|
|
153
|
+
"deep-agent",
|
|
154
|
+
image="python:3.13",
|
|
155
|
+
memory=1024,
|
|
156
|
+
cpus=2,
|
|
157
|
+
ephemeral=True,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Create a DeepAgent MicroSandbox Backend
|
|
161
|
+
backend = await MicrosandboxSandbox.create(
|
|
162
|
+
sandbox,
|
|
163
|
+
timeout=1800,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
agent = create_deep_agent(model=model, backend=backend)
|
|
168
|
+
result = await agent.ainvoke(
|
|
169
|
+
{
|
|
170
|
+
"messages": [
|
|
171
|
+
{
|
|
172
|
+
"role": "user",
|
|
173
|
+
"content": "Write 'Hi !' in the /tmp/test.txt",
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
},
|
|
177
|
+
)
|
|
178
|
+
finally:
|
|
179
|
+
content_file = await backend.adownload_files(["/tmp/test.txt"])
|
|
180
|
+
print(content_file)
|
|
181
|
+
# [FileDownloadResponse(path='/tmp/test', content=b'Hi !', error=None)]
|
|
182
|
+
|
|
183
|
+
# Sandbox lifecycle management remains the caller's responsibility.
|
|
184
|
+
await sandbox.stop()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Sandbox lifecyle
|
|
188
|
+
|
|
189
|
+
**The adapter does not create, start, stop, or destroy virtual machines.**
|
|
190
|
+
|
|
191
|
+
The calling application remains responsible for:
|
|
192
|
+
|
|
193
|
+
* Creating and configuring the Microsandbox instance.
|
|
194
|
+
* Managing its lifecycle.
|
|
195
|
+
* Defining its persistence policy.
|
|
196
|
+
* Stopping and cleaning up the sandbox when execution is complete.
|
|
197
|
+
|
|
198
|
+
Setting `ephemeral=True` when creating the sandbox allows Microsandbox to apply its ephemeral VM lifecycle policy (destroyed after stopping).
|
|
199
|
+
|
|
200
|
+
The adapter wraps an existing Microsandbox instance. Sandbox creation, lifecycle management and persistence remain the responsibility of the calling application.
|
|
201
|
+
|
|
202
|
+
> [!WARNING]
|
|
203
|
+
> The Microsandbox VM must have **Python 3 installed and available in PATH**. Without it, inherited `BaseSandbox` filesystem operations may fail.
|
|
204
|
+
|
|
205
|
+
## Configuration
|
|
206
|
+
|
|
207
|
+
`MicrosandboxSandbox.create()` accepts the following parameters:
|
|
208
|
+
|
|
209
|
+
| Parameter | Type | Default | Description |
|
|
210
|
+
|---|---|---|---|
|
|
211
|
+
| `sandbox` | `Sandbox` | Required | Existing Microsandbox instance. |
|
|
212
|
+
| `timeout` | `int` | `1800` | Default command execution timeout in seconds. Use `0` to disable it. Must be positive. |
|
|
213
|
+
| `enable_capture_offload` | `bool` | `False` | Enables capture-at-source offloading for large command outputs. Enabling it requires a compatible shell environment and the utilities expected by Deep Agents' capture wrapper. |
|
|
214
|
+
| `grep_exclude_dirs` | `tuple[str, ...]` | `(".git",)` | Directory names excluded from recursive grep searches. |
|
|
215
|
+
|
|
216
|
+
### Example
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
backend = await MicrosandboxSandbox.create(
|
|
220
|
+
sandbox,
|
|
221
|
+
timeout=600,
|
|
222
|
+
enable_capture_offload=False,
|
|
223
|
+
grep_exclude_dirs=(".git", "__pycache__", ".venv"),
|
|
224
|
+
)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Testing
|
|
228
|
+
|
|
229
|
+
Tests use a real Microsandbox VM.
|
|
230
|
+
|
|
231
|
+
**A working Microsandbox runtime and hardware virtualization support are required.**
|
|
232
|
+
|
|
233
|
+
### Install development dependencies:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
uv sync --group dev
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Run the test suite:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
uv run pytest
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Run with verbose output and coverage
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
uv run pytest -v -s --tb=short --cov --cov-report=term-missing
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## License
|
|
252
|
+
|
|
253
|
+
This project is distributed under the MIT License.
|
|
254
|
+
|
|
255
|
+
## Acknowledgements
|
|
256
|
+
|
|
257
|
+
This project builds upon the `BaseSandbox` interface provided by
|
|
258
|
+
[LangChain Deep Agents](https://github.com/langchain-ai/deepagents).
|
|
259
|
+
|
|
260
|
+
It is inspired by `kenwoodjw/langchain-microsandbox`, whose work helped establish the initial integration approach.
|
|
261
|
+
|
|
262
|
+
Deep Agents is distributed under the MIT License.
|
|
263
|
+
|
|
264
|
+
`langchain-backend-microsandbox` is an **independent community integration** and is not affiliated with, endorsed by, or officially supported by LangChain.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# langchain-backend-microsandbox
|
|
2
|
+
|
|
3
|
+
**A [Microsandbox](https://github.com/superradcompany/microsandbox) backend for [LangChain Deep Agents](https://github.com/langchain-ai/deepagents), bringing isolated microVM execution to autonomous AI agents.**
|
|
4
|
+
|
|
5
|
+
Run agent-generated commands and manage files inside lightweight, isolated virtual machines through Deep Agents' native BaseSandbox interface.
|
|
6
|
+
|
|
7
|
+
Built on Microsandbox, this integration provides a seamless bridge between Deep Agents and Microsandbox's asynchronous execution and filesystem APIs.
|
|
8
|
+
|
|
9
|
+
Inspired by `kenwoodjw/langchain-microsandbox`, this implementation extends the original approach with a focus on compatibility, predictable behavior, and closer adherence to the BaseSandbox contract.
|
|
10
|
+
|
|
11
|
+
## Why Microsandbox?
|
|
12
|
+
|
|
13
|
+
AI agents increasingly need to execute code, manipulate files, and interact with shell environments to accomplish complex tasks.
|
|
14
|
+
|
|
15
|
+
Running these operations directly on the host introduces unnecessary security risks.
|
|
16
|
+
|
|
17
|
+
Microsandbox addresses this challenge by providing isolated microVM environments in which agent workloads can execute independently of the host system.
|
|
18
|
+
|
|
19
|
+
langchain-backend-microsandbox brings this isolation model to LangChain Deep Agents, allowing developers to use Microsandbox as their agent execution backend without implementing their own sandbox integration.
|
|
20
|
+
|
|
21
|
+
**The result: a standardized Deep Agents backend with isolated execution and native filesystem support.**
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
| Feature | Description |
|
|
26
|
+
|---|---|
|
|
27
|
+
| **Deep Agents integration** | Implements the `BaseSandbox` interface. |
|
|
28
|
+
| **Contract compliance** | Designed to closely follow the `BaseSandbox` behavior and expectations. |
|
|
29
|
+
| **Isolated execution** | Runs agent-generated commands inside Microsandbox microVMs. |
|
|
30
|
+
| **Async & sync execution** | Supports both synchronous and asynchronous command execution. |
|
|
31
|
+
| **Native file transfers** | Binary-safe file uploads and downloads through Microsandbox's filesystem API. |
|
|
32
|
+
| **Filesystem operations** | Inherits `read`, `write`, `edit`, `delete`, `ls`, `grep`, and `glob` from `BaseSandbox`. |
|
|
33
|
+
| **Timeout management** | Configurable execution timeouts with standardized timeout exit codes. |
|
|
34
|
+
| **Error normalization** | Maps filesystem exceptions to Deep Agents-compatible error codes. |
|
|
35
|
+
| **Atomic file uploads** | Uses temporary files and filesystem rename operations to avoid exposing partially written files. |
|
|
36
|
+
|
|
37
|
+
## Architecture & Compatibility
|
|
38
|
+
|
|
39
|
+
MicrosandboxSandbox acts as an adapter between LangChain Deep Agents and an existing Microsandbox instance.
|
|
40
|
+
|
|
41
|
+
It implements the backend-specific operations while relying on BaseSandbox for the higher-level filesystem functionality.
|
|
42
|
+
|
|
43
|
+
### Inherited BaseSandbox operations
|
|
44
|
+
|
|
45
|
+
The following operations are inherited directly from LangChain Deep Agents:
|
|
46
|
+
|
|
47
|
+
* `read()`, `write()`, `edit()`, `delete()`
|
|
48
|
+
* `ls()`, `grep()`, `glob()`
|
|
49
|
+
|
|
50
|
+
This approach avoids reimplementing filesystem behavior and helps maintain consistency with other Deep Agents sandbox backends.
|
|
51
|
+
|
|
52
|
+
### BaseSandbox behavior
|
|
53
|
+
|
|
54
|
+
#### Grep directory exclusions
|
|
55
|
+
|
|
56
|
+
Directories listed in `grep_exclude_dirs` are excluded from recursive searches, regardless of their depth in the directory tree.
|
|
57
|
+
|
|
58
|
+
However, explicitly specifying an excluded directory as the search root overrides this exclusion.
|
|
59
|
+
|
|
60
|
+
In other words:
|
|
61
|
+
- Searching from a parent directory skips excluded directories.
|
|
62
|
+
- Explicitly targeting an excluded directory still searches its contents.
|
|
63
|
+
|
|
64
|
+
This allows common directories such as `.git` to be excluded from general searches without preventing developers or agents from inspecting them when needed.
|
|
65
|
+
|
|
66
|
+
Reference: [BaseSandbox.grep_exclude_dirs](https://reference.langchain.com/python/deepagents/backends/sandbox/BaseSandbox/grep_exclude_dirs)
|
|
67
|
+
|
|
68
|
+
#### Command execution timeouts
|
|
69
|
+
|
|
70
|
+
Timeout behavior follows the `SandboxBackendProtocol` conventions:
|
|
71
|
+
|
|
72
|
+
- `None` uses the default timeout configured when creating the adapter.
|
|
73
|
+
- `0` disables the execution timeout.
|
|
74
|
+
- Negative values are rejected with a `ValueError`.
|
|
75
|
+
- When a command exceeds its configured timeout, the backend returns exit code `124`.
|
|
76
|
+
|
|
77
|
+
Reference: [SandboxBackendProtocol.execute](https://reference.langchain.com/python/deepagents/backends/protocol/SandboxBackendProtocol/execute)
|
|
78
|
+
|
|
79
|
+
#### Python runtime requirement
|
|
80
|
+
|
|
81
|
+
The Microsandbox VM must have Python 3 installed and available in `PATH`.
|
|
82
|
+
|
|
83
|
+
This is a requirement of the inherited `BaseSandbox` filesystem operations, which rely on Python scripts executed inside the sandbox.
|
|
84
|
+
|
|
85
|
+
Having Python installed on the host machine is not sufficient: the interpreter must be available inside the Microsandbox VM.
|
|
86
|
+
|
|
87
|
+
Make sure your sandbox image includes a compatible Python installation before initializing the backend.
|
|
88
|
+
|
|
89
|
+
## Requirements
|
|
90
|
+
|
|
91
|
+
- Python >= 3.12
|
|
92
|
+
- [Deep Agents](https://github.com/langchain-ai/deepagents) >= 0.7.35, < 0.8.0
|
|
93
|
+
- [Microsandbox](https://github.com/superradcompany/microsandbox) >= 0.7.14, < 0.8.0
|
|
94
|
+
- A supported Microsandbox runtime environment.
|
|
95
|
+
|
|
96
|
+
Microsandbox requires hardware virtualization support:
|
|
97
|
+
- **Linux**: KVM
|
|
98
|
+
- **macOS**: Apple Silicon virtualization support
|
|
99
|
+
- **Windows**: Windows Hypervisor Platform (WHP)
|
|
100
|
+
|
|
101
|
+
On **debian/ubuntu system**, you can install KVM by installing:
|
|
102
|
+
|
|
103
|
+
`sudo apt install qemu-system libvirt-daemon-system`
|
|
104
|
+
|
|
105
|
+
## Installation from sources
|
|
106
|
+
|
|
107
|
+
Clone the repository and install the package:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/dod-ia/langchain-backend-microsandbox.git
|
|
111
|
+
|
|
112
|
+
cd langchain-backend-microsandbox
|
|
113
|
+
|
|
114
|
+
uv sync
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Get started
|
|
118
|
+
|
|
119
|
+
The adapter wraps an existing Microsandbox instance and exposes it as a Deep Agents backend.
|
|
120
|
+
|
|
121
|
+
Since Microsandbox provides an asynchronous API, sandbox creation and adapter initialization must be performed asynchronously.
|
|
122
|
+
|
|
123
|
+
### Basic example
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from microsandbox import Sandbox
|
|
127
|
+
from langchain_backend_microsandbox import MicrosandboxSandbox
|
|
128
|
+
from deepagents import create_deep_agent
|
|
129
|
+
|
|
130
|
+
model = init_chat_model("YOUR_MODEL")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def main():
|
|
134
|
+
# Create a sandbox from MicroSandbox API
|
|
135
|
+
sandbox = await Sandbox.create(
|
|
136
|
+
"deep-agent",
|
|
137
|
+
image="python:3.13",
|
|
138
|
+
memory=1024,
|
|
139
|
+
cpus=2,
|
|
140
|
+
ephemeral=True,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Create a DeepAgent MicroSandbox Backend
|
|
144
|
+
backend = await MicrosandboxSandbox.create(
|
|
145
|
+
sandbox,
|
|
146
|
+
timeout=1800,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
agent = create_deep_agent(model=model, backend=backend)
|
|
151
|
+
result = await agent.ainvoke(
|
|
152
|
+
{
|
|
153
|
+
"messages": [
|
|
154
|
+
{
|
|
155
|
+
"role": "user",
|
|
156
|
+
"content": "Write 'Hi !' in the /tmp/test.txt",
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
finally:
|
|
162
|
+
content_file = await backend.adownload_files(["/tmp/test.txt"])
|
|
163
|
+
print(content_file)
|
|
164
|
+
# [FileDownloadResponse(path='/tmp/test', content=b'Hi !', error=None)]
|
|
165
|
+
|
|
166
|
+
# Sandbox lifecycle management remains the caller's responsibility.
|
|
167
|
+
await sandbox.stop()
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Sandbox lifecyle
|
|
171
|
+
|
|
172
|
+
**The adapter does not create, start, stop, or destroy virtual machines.**
|
|
173
|
+
|
|
174
|
+
The calling application remains responsible for:
|
|
175
|
+
|
|
176
|
+
* Creating and configuring the Microsandbox instance.
|
|
177
|
+
* Managing its lifecycle.
|
|
178
|
+
* Defining its persistence policy.
|
|
179
|
+
* Stopping and cleaning up the sandbox when execution is complete.
|
|
180
|
+
|
|
181
|
+
Setting `ephemeral=True` when creating the sandbox allows Microsandbox to apply its ephemeral VM lifecycle policy (destroyed after stopping).
|
|
182
|
+
|
|
183
|
+
The adapter wraps an existing Microsandbox instance. Sandbox creation, lifecycle management and persistence remain the responsibility of the calling application.
|
|
184
|
+
|
|
185
|
+
> [!WARNING]
|
|
186
|
+
> The Microsandbox VM must have **Python 3 installed and available in PATH**. Without it, inherited `BaseSandbox` filesystem operations may fail.
|
|
187
|
+
|
|
188
|
+
## Configuration
|
|
189
|
+
|
|
190
|
+
`MicrosandboxSandbox.create()` accepts the following parameters:
|
|
191
|
+
|
|
192
|
+
| Parameter | Type | Default | Description |
|
|
193
|
+
|---|---|---|---|
|
|
194
|
+
| `sandbox` | `Sandbox` | Required | Existing Microsandbox instance. |
|
|
195
|
+
| `timeout` | `int` | `1800` | Default command execution timeout in seconds. Use `0` to disable it. Must be positive. |
|
|
196
|
+
| `enable_capture_offload` | `bool` | `False` | Enables capture-at-source offloading for large command outputs. Enabling it requires a compatible shell environment and the utilities expected by Deep Agents' capture wrapper. |
|
|
197
|
+
| `grep_exclude_dirs` | `tuple[str, ...]` | `(".git",)` | Directory names excluded from recursive grep searches. |
|
|
198
|
+
|
|
199
|
+
### Example
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
backend = await MicrosandboxSandbox.create(
|
|
203
|
+
sandbox,
|
|
204
|
+
timeout=600,
|
|
205
|
+
enable_capture_offload=False,
|
|
206
|
+
grep_exclude_dirs=(".git", "__pycache__", ".venv"),
|
|
207
|
+
)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Testing
|
|
211
|
+
|
|
212
|
+
Tests use a real Microsandbox VM.
|
|
213
|
+
|
|
214
|
+
**A working Microsandbox runtime and hardware virtualization support are required.**
|
|
215
|
+
|
|
216
|
+
### Install development dependencies:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
uv sync --group dev
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Run the test suite:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
uv run pytest
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Run with verbose output and coverage
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
uv run pytest -v -s --tb=short --cov --cov-report=term-missing
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
This project is distributed under the MIT License.
|
|
237
|
+
|
|
238
|
+
## Acknowledgements
|
|
239
|
+
|
|
240
|
+
This project builds upon the `BaseSandbox` interface provided by
|
|
241
|
+
[LangChain Deep Agents](https://github.com/langchain-ai/deepagents).
|
|
242
|
+
|
|
243
|
+
It is inspired by `kenwoodjw/langchain-microsandbox`, whose work helped establish the initial integration approach.
|
|
244
|
+
|
|
245
|
+
Deep Agents is distributed under the MIT License.
|
|
246
|
+
|
|
247
|
+
`langchain-backend-microsandbox` is an **independent community integration** and is not affiliated with, endorsed by, or officially supported by LangChain.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-backend-microsandbox"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Microsandbox backend integration for DeepAgents and LangChain."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
keywords = ["deepagents", "langchain", "microsandbox", "sandbox", "microvm"]
|
|
13
|
+
authors = [{ name = "dod-ia"}]
|
|
14
|
+
requires-python = ">=3.12"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"deepagents>=0.7.35,<0.8.0",
|
|
17
|
+
"microsandbox>=0.7.14,<0.8.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://github.com/dod-ia/langchain-backend-microsandbox"
|
|
22
|
+
Repository = "https://github.com/dod-ia/langchain-backend-microsandbox"
|
|
23
|
+
Issues = "https://github.com/dod-ia/langchain-backend-microsandbox/issues"
|
|
24
|
+
Changelog = "https://github.com/dod-ia/langchain-backend-microsandbox/CHANGELOG.md"
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.targets.wheel]
|
|
27
|
+
packages = ["src/langchain_backend_microsandbox"]
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.sdist]
|
|
30
|
+
include = [
|
|
31
|
+
"src/",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest>=9.2.0",
|
|
39
|
+
"pytest-asyncio>=1.4.1.post1",
|
|
40
|
+
"pytest-cov>=7.1.0",
|
|
41
|
+
"ruff>=0.17.4",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.format]
|
|
45
|
+
docstring-code-format = true
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = ["ALL"]
|
|
49
|
+
ignore = [
|
|
50
|
+
"COM812", # Messes with the formatter
|
|
51
|
+
"ISC001", # Messes with the formatter
|
|
52
|
+
"ASYNC109", # Timeout is delegated to microVM framework
|
|
53
|
+
"TRY003"
|
|
54
|
+
]
|
|
55
|
+
extend-safe-fixes = ["PLR6201"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff.lint.pydocstyle]
|
|
58
|
+
convention = "google"
|
|
59
|
+
ignore-var-parameters = true
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
62
|
+
ban-relative-imports = "all"
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
extend-exclude = ["tests"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Original work Copyright (c) 2026 LangChain
|
|
2
|
+
# Modifications Copyright (c) 2026 dod-ia
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Microsandbox integration for Deep Agents."""
|
|
6
|
+
|
|
7
|
+
from langchain_backend_microsandbox.backend import MicrosandboxSandbox
|
|
8
|
+
|
|
9
|
+
__all__ = ["MicrosandboxSandbox"]
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# Original work Copyright (c) 2026 LangChain
|
|
2
|
+
# Modifications Copyright (c) 2026 dod-ia
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
"""Microsandbox backend integration for LangChain Deep Agents.
|
|
6
|
+
|
|
7
|
+
Provides a BaseSandbox implementation backed by Microsandbox.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import uuid
|
|
14
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
15
|
+
from contextlib import suppress
|
|
16
|
+
from pathlib import PurePosixPath
|
|
17
|
+
from typing import TYPE_CHECKING, cast
|
|
18
|
+
|
|
19
|
+
from deepagents.backends.protocol import (
|
|
20
|
+
FILE_NOT_FOUND,
|
|
21
|
+
INVALID_PATH,
|
|
22
|
+
IS_DIRECTORY,
|
|
23
|
+
PERMISSION_DENIED,
|
|
24
|
+
ExecuteResponse,
|
|
25
|
+
FileDownloadResponse,
|
|
26
|
+
FileOperationError,
|
|
27
|
+
FileUploadResponse,
|
|
28
|
+
)
|
|
29
|
+
from deepagents.backends.sandbox import BaseSandbox
|
|
30
|
+
from microsandbox import (
|
|
31
|
+
ExecTimeoutError,
|
|
32
|
+
FilesystemError,
|
|
33
|
+
PathNotFoundError,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
38
|
+
from concurrent.futures import Future
|
|
39
|
+
|
|
40
|
+
from microsandbox import ExecOutput, Sandbox
|
|
41
|
+
|
|
42
|
+
_TIMEOUT_EXIT_CODE = 124
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class MicrosandboxSandbox(BaseSandbox):
|
|
46
|
+
"""Sandbox backend implementation powered by Microsandbox.
|
|
47
|
+
|
|
48
|
+
Wraps an existing Microsandbox instance to expose the synchronous and
|
|
49
|
+
asynchronous interfaces expected by Deep Agents (conforming to `SandboxBackendProtocol`).
|
|
50
|
+
|
|
51
|
+
The adapter does not manage the sandbox lifecycle. The caller remains
|
|
52
|
+
responsible for starting, stopping, and configuring the sandbox, as well
|
|
53
|
+
as defining its persistence and cleanup policies.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
sandbox: Sandbox,
|
|
60
|
+
sandbox_id: str,
|
|
61
|
+
timeout: int,
|
|
62
|
+
enable_capture_offload: bool = False,
|
|
63
|
+
grep_exclude_dirs: tuple[str, ...] = (".git",),
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Initialize the adapter with an existing Microsandbox instance.
|
|
66
|
+
|
|
67
|
+
Prefer using the asynchronous `create()` factory, which resolves the
|
|
68
|
+
sandbox identifier before constructing the adapter.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
sandbox: Microsandbox instance to wrap.
|
|
72
|
+
sandbox_id: Resolved name of the sandbox, cached for synchronous access
|
|
73
|
+
through the `id` property.
|
|
74
|
+
timeout: Default command execution timeout in seconds. Must be
|
|
75
|
+
non-negative. A value of zero disables the execution timeout.
|
|
76
|
+
enable_capture_offload: Whether to enable Deep Agents' capture-at-source
|
|
77
|
+
mechanism for large command outputs. Requires a compatible shell
|
|
78
|
+
environment and the utilities used by the capture wrapper.
|
|
79
|
+
grep_exclude_dirs: Directory names excluded from grep searches,
|
|
80
|
+
regardless of their depth in the directory tree.
|
|
81
|
+
"""
|
|
82
|
+
self._sandbox = sandbox
|
|
83
|
+
self._id = sandbox_id
|
|
84
|
+
self._default_timeout = timeout
|
|
85
|
+
self.enable_capture_offload = enable_capture_offload
|
|
86
|
+
self.grep_exclude_dirs = grep_exclude_dirs
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
async def create(
|
|
90
|
+
cls,
|
|
91
|
+
sandbox: Sandbox,
|
|
92
|
+
*,
|
|
93
|
+
timeout: int = 30 * 60,
|
|
94
|
+
enable_capture_offload: bool = False,
|
|
95
|
+
grep_exclude_dirs: tuple[str, ...] = (".git",),
|
|
96
|
+
) -> MicrosandboxSandbox:
|
|
97
|
+
"""Asynchronously create an adapter around an existing Microsandbox instance.
|
|
98
|
+
|
|
99
|
+
Resolves the sandbox name through the provider API and caches it for
|
|
100
|
+
subsequent synchronous access through the `id` property.
|
|
101
|
+
|
|
102
|
+
The sandbox image must provide Python 3 in PATH, as inherited BaseSandbox
|
|
103
|
+
operations execute Python scripts inside the sandbox.
|
|
104
|
+
|
|
105
|
+
Capture offloading is disabled by default. Enabling it requires a
|
|
106
|
+
compatible shell environment and the utilities expected by Deep Agents'
|
|
107
|
+
capture wrapper.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
sandbox: Microsandbox instance to wrap.
|
|
111
|
+
timeout: Default command execution timeout in seconds.
|
|
112
|
+
Defaults to 1800 seconds. A value of zero disables the timeout.
|
|
113
|
+
enable_capture_offload: Whether to enable capture-at-source offloading
|
|
114
|
+
for large command outputs. Requires a compatible shell and
|
|
115
|
+
coreutils environment.
|
|
116
|
+
grep_exclude_dirs: Directory names excluded from grep searches at
|
|
117
|
+
any depth. Defaults to (".git",).
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
An initialized MicrosandboxSandbox adapter.
|
|
121
|
+
|
|
122
|
+
Raises:
|
|
123
|
+
ValueError: If `timeout` is negative.
|
|
124
|
+
"""
|
|
125
|
+
if timeout < 0:
|
|
126
|
+
msg = f"timeout must be non-negative, got {timeout}"
|
|
127
|
+
raise ValueError(msg)
|
|
128
|
+
# v0.6.12 exposes an awaitable property at runtime even though its stub
|
|
129
|
+
# declares an async method; accept both forms across the supported range.
|
|
130
|
+
name: object = sandbox.name
|
|
131
|
+
if callable(name):
|
|
132
|
+
name_factory = cast("Callable[[], Awaitable[str]]", name)
|
|
133
|
+
sandbox_id = await name_factory()
|
|
134
|
+
else:
|
|
135
|
+
sandbox_id = await cast("Awaitable[str]", name)
|
|
136
|
+
return cls(
|
|
137
|
+
sandbox=sandbox,
|
|
138
|
+
sandbox_id=sandbox_id,
|
|
139
|
+
timeout=timeout,
|
|
140
|
+
enable_capture_offload=enable_capture_offload,
|
|
141
|
+
grep_exclude_dirs=grep_exclude_dirs,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def id(self) -> str:
|
|
146
|
+
"""Return the cached name of the underlying Microsandbox instance.
|
|
147
|
+
|
|
148
|
+
The identifier is resolved asynchronously during adapter creation,
|
|
149
|
+
allowing it to be exposed through the synchronous BaseSandbox interface.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
The sandbox name.
|
|
153
|
+
"""
|
|
154
|
+
return self._id
|
|
155
|
+
|
|
156
|
+
def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse:
|
|
157
|
+
"""Execute a shell command through the synchronous backend interface.
|
|
158
|
+
|
|
159
|
+
Delegates execution to `aexecute()` and bridges the asynchronous provider
|
|
160
|
+
API to the synchronous interface.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
command: Shell command to execute inside the sandbox.
|
|
164
|
+
timeout: Maximum execution time in seconds. If `None`, uses the
|
|
165
|
+
default timeout configured when the adapter was created.
|
|
166
|
+
A value of zero disables the timeout. Must be positive.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
An ExecuteResponse containing the combined command output and
|
|
170
|
+
its exit status.
|
|
171
|
+
"""
|
|
172
|
+
return _run_sync(lambda: self.aexecute(command, timeout=timeout))
|
|
173
|
+
|
|
174
|
+
async def aexecute(
|
|
175
|
+
self, command: str, *, timeout: int | None = None
|
|
176
|
+
) -> ExecuteResponse:
|
|
177
|
+
"""Execute a shell command using Microsandbox's asynchronous API.
|
|
178
|
+
|
|
179
|
+
Uses the configured default timeout unless an execution-specific timeout
|
|
180
|
+
is provided. A timeout of zero disables the provider-side timeout.
|
|
181
|
+
|
|
182
|
+
Provider execution timeouts are converted into an ExecuteResponse with
|
|
183
|
+
exit code 124, matching the behavior of other Deep Agents sandbox
|
|
184
|
+
integrations.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
command: Shell command to execute inside the sandbox.
|
|
188
|
+
timeout: Maximum execution time in seconds. If `None`, uses the
|
|
189
|
+
default timeout configured when the adapter was created.
|
|
190
|
+
A value of zero disables the timeout. Must be positive.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
An ExecuteResponse containing the combined command output and its exit status.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ValueError: If the effective timeout is negative.
|
|
197
|
+
"""
|
|
198
|
+
effective_timeout = self._default_timeout if timeout is None else timeout
|
|
199
|
+
|
|
200
|
+
if effective_timeout < 0:
|
|
201
|
+
msg = f"timeout must be non-negative, got {effective_timeout}"
|
|
202
|
+
raise ValueError(msg)
|
|
203
|
+
|
|
204
|
+
effective_timeout = None if effective_timeout == 0 else float(effective_timeout)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
output = await self._sandbox.shell(command, timeout=effective_timeout)
|
|
208
|
+
except ExecTimeoutError:
|
|
209
|
+
msg = f"Command timed out after {effective_timeout} seconds"
|
|
210
|
+
return ExecuteResponse(
|
|
211
|
+
output=msg, exit_code=_TIMEOUT_EXIT_CODE, truncated=False
|
|
212
|
+
)
|
|
213
|
+
return _map_execute_output(output)
|
|
214
|
+
|
|
215
|
+
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
216
|
+
"""Upload files through the synchronous backend interface.
|
|
217
|
+
|
|
218
|
+
Delegates file uploads to `aupload_files()` and bridges the asynchronous
|
|
219
|
+
filesystem API to the synchronous BaseSandbox interface.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
files: List of pairs containing an absolute sandbox path and the
|
|
223
|
+
corresponding file contents as bytes.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
A list of FileUploadResponse objects, preserving the input order.
|
|
227
|
+
Individual upload failures are reported in their corresponding
|
|
228
|
+
response rather than raised.
|
|
229
|
+
"""
|
|
230
|
+
return _run_sync(lambda: self.aupload_files(files))
|
|
231
|
+
|
|
232
|
+
async def aupload_files(
|
|
233
|
+
self, files: list[tuple[str, bytes]]
|
|
234
|
+
) -> list[FileUploadResponse]:
|
|
235
|
+
"""Upload multiple files using Microsandbox's asynchronous filesystem API.
|
|
236
|
+
|
|
237
|
+
Processes files sequentially and delegates each upload to `_aupload_file()`.
|
|
238
|
+
|
|
239
|
+
Each file is handled independently: a failed upload does not prevent
|
|
240
|
+
subsequent files from being processed.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
files: List of pairs containing an absolute sandbox path and the
|
|
244
|
+
corresponding file contents as bytes.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
A list of FileUploadResponse objects, preserving the input order.
|
|
248
|
+
Each response contains either a successful upload result or an
|
|
249
|
+
error describing the failure.
|
|
250
|
+
"""
|
|
251
|
+
return [await self._aupload_file(path, content) for path, content in files]
|
|
252
|
+
|
|
253
|
+
async def _aupload_file(self, path: str, content: bytes) -> FileUploadResponse:
|
|
254
|
+
"""Upload a single file atomically to the sandbox filesystem.
|
|
255
|
+
|
|
256
|
+
Creates the parent directory when necessary, writes the content to a
|
|
257
|
+
temporary file in the destination directory, then renames it to the
|
|
258
|
+
requested path.
|
|
259
|
+
|
|
260
|
+
Using a temporary file prevents readers from observing partially
|
|
261
|
+
written file contents when the underlying filesystem supports atomic
|
|
262
|
+
rename operations.
|
|
263
|
+
|
|
264
|
+
The temporary file is removed on a best-effort basis, including when
|
|
265
|
+
the upload fails.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
path: Absolute destination path inside the sandbox.
|
|
269
|
+
content: File contents to upload.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
A FileUploadResponse containing the destination path and either
|
|
273
|
+
`None` on success or a normalized filesystem error.
|
|
274
|
+
"""
|
|
275
|
+
if not path.startswith("/") or "\x00" in path:
|
|
276
|
+
return FileUploadResponse(path=path, error=INVALID_PATH)
|
|
277
|
+
|
|
278
|
+
temp_path = None
|
|
279
|
+
try:
|
|
280
|
+
parent = str(PurePosixPath(path).parent)
|
|
281
|
+
if parent != "/":
|
|
282
|
+
await self._sandbox.fs.mkdir(parent)
|
|
283
|
+
temp_path = f"{path}.{uuid.uuid4().hex}.tmp"
|
|
284
|
+
await self._sandbox.fs.write(temp_path, content)
|
|
285
|
+
# `rename` is based on tokio::fs::rename (crates/agentd/lib/fs.rs)
|
|
286
|
+
# This function is based on Linux rename so it is atomic in a Linux system.
|
|
287
|
+
await self._sandbox.fs.rename(temp_path, path)
|
|
288
|
+
except Exception as exc: # noqa: BLE001
|
|
289
|
+
return FileUploadResponse(path=path, error=_map_file_error(exc))
|
|
290
|
+
finally:
|
|
291
|
+
if temp_path is not None:
|
|
292
|
+
with suppress(Exception):
|
|
293
|
+
await self._sandbox.fs.remove(temp_path)
|
|
294
|
+
return FileUploadResponse(path=path, error=None)
|
|
295
|
+
|
|
296
|
+
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
297
|
+
"""Download files through the synchronous backend interface.
|
|
298
|
+
|
|
299
|
+
Delegates file downloads to `adownload_files()` and bridges the
|
|
300
|
+
asynchronous filesystem API to the synchronous BaseSandbox interface.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
paths: List of absolute sandbox paths to download.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
A list of FileDownloadResponse objects, preserving the input order.
|
|
307
|
+
Individual download failures are reported in their corresponding
|
|
308
|
+
response rather than raised.
|
|
309
|
+
"""
|
|
310
|
+
return _run_sync(lambda: self.adownload_files(paths))
|
|
311
|
+
|
|
312
|
+
async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
313
|
+
"""Download multiple files using Microsandbox's asynchronous filesystem API.
|
|
314
|
+
|
|
315
|
+
Processes paths sequentially and delegates each download to
|
|
316
|
+
`_adownload_file()`.
|
|
317
|
+
|
|
318
|
+
Each path is handled independently: a failed download does not prevent
|
|
319
|
+
subsequent paths from being processed.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
paths: List of absolute sandbox paths to download.
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
A list of FileDownloadResponse objects, preserving the input order.
|
|
326
|
+
Each response contains the file contents on success or a normalized
|
|
327
|
+
error when the download fails.
|
|
328
|
+
"""
|
|
329
|
+
return [await self._adownload_file(path) for path in paths]
|
|
330
|
+
|
|
331
|
+
async def _adownload_file(self, path: str) -> FileDownloadResponse:
|
|
332
|
+
"""Download a single file from the sandbox filesystem.
|
|
333
|
+
|
|
334
|
+
Reads the requested path using Microsandbox's asynchronous filesystem API.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
path: Absolute path of the file to download inside the sandbox.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
A FileDownloadResponse containing the file contents on success.
|
|
341
|
+
If the path is invalid or the filesystem operation fails, the
|
|
342
|
+
response contains `None` as its content and a corresponding error.
|
|
343
|
+
"""
|
|
344
|
+
if not path.startswith("/") or "\x00" in path:
|
|
345
|
+
return FileDownloadResponse(path=path, content=None, error=INVALID_PATH)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
content = await self._sandbox.fs.read(path)
|
|
349
|
+
except Exception as exc: # noqa: BLE001
|
|
350
|
+
return FileDownloadResponse(
|
|
351
|
+
path=path, content=None, error=_map_file_error(exc)
|
|
352
|
+
)
|
|
353
|
+
return FileDownloadResponse(path=path, content=content, error=None)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _map_execute_output(output: ExecOutput) -> ExecuteResponse:
|
|
357
|
+
"""Convert a Microsandbox execution result into a Deep Agents response.
|
|
358
|
+
|
|
359
|
+
Decodes stdout and stderr as UTF-8, replacing invalid byte sequences
|
|
360
|
+
instead of raising decoding errors.
|
|
361
|
+
|
|
362
|
+
Stdout is preserved as the primary output. When stderr is not empty,
|
|
363
|
+
it is appended using `<stderr>` tags, with a newline separator when
|
|
364
|
+
necessary.
|
|
365
|
+
|
|
366
|
+
The provider's exit code is preserved. Output truncation is not
|
|
367
|
+
performed by this adapter.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
output: Execution result returned by Microsandbox.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
An ExecuteResponse containing the combined output, the provider's
|
|
374
|
+
exit code, and a `truncated` flag set to False.
|
|
375
|
+
"""
|
|
376
|
+
stdout = bytes(output.stdout_bytes).decode("utf-8", errors="replace")
|
|
377
|
+
stderr = bytes(output.stderr_bytes).decode("utf-8", errors="replace")
|
|
378
|
+
|
|
379
|
+
combined = stdout
|
|
380
|
+
if stderr:
|
|
381
|
+
separator = "" if not stdout or stdout.endswith(("\n", "\r")) else "\n"
|
|
382
|
+
combined += f"{separator}<stderr>{stderr}</stderr>"
|
|
383
|
+
return ExecuteResponse(output=combined, exit_code=output.exit_code, truncated=False)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _map_file_error(exc: Exception) -> FileOperationError | str: # noqa: PLR0911
|
|
387
|
+
"""Normalize filesystem exceptions into Deep Agents file operation errors.
|
|
388
|
+
|
|
389
|
+
Maps known Python and Microsandbox filesystem exceptions to the
|
|
390
|
+
corresponding FileOperationError constants.
|
|
391
|
+
|
|
392
|
+
For provider exceptions that do not expose structured error types,
|
|
393
|
+
the exception message is inspected to identify common filesystem
|
|
394
|
+
failures, such as permission errors, missing files, and invalid paths.
|
|
395
|
+
|
|
396
|
+
Unrecognized exceptions are preserved as descriptive strings so that
|
|
397
|
+
callers retain access to the original error information.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
exc: Exception raised by a filesystem operation.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
A normalized FileOperationError when the failure matches a known
|
|
404
|
+
category, otherwise a string containing the exception type and
|
|
405
|
+
its message.
|
|
406
|
+
"""
|
|
407
|
+
if isinstance(exc, PermissionError):
|
|
408
|
+
return PERMISSION_DENIED
|
|
409
|
+
if isinstance(exc, IsADirectoryError):
|
|
410
|
+
return IS_DIRECTORY
|
|
411
|
+
if isinstance(exc, NotADirectoryError):
|
|
412
|
+
return INVALID_PATH
|
|
413
|
+
if isinstance(exc, (FileNotFoundError, PathNotFoundError)):
|
|
414
|
+
return FILE_NOT_FOUND
|
|
415
|
+
|
|
416
|
+
if isinstance(exc, FilesystemError):
|
|
417
|
+
message = str(exc).lower()
|
|
418
|
+
substring_errors: tuple[tuple[tuple[str, ...], FileOperationError], ...] = (
|
|
419
|
+
(("permission denied", "access denied", "forbidden"), PERMISSION_DENIED),
|
|
420
|
+
(("is a directory",), IS_DIRECTORY),
|
|
421
|
+
(("not a directory", "invalid path"), INVALID_PATH),
|
|
422
|
+
(("no such file", "not found"), FILE_NOT_FOUND),
|
|
423
|
+
)
|
|
424
|
+
for needles, error in substring_errors:
|
|
425
|
+
if any(needle in message for needle in needles):
|
|
426
|
+
return error
|
|
427
|
+
detail = str(exc).strip()
|
|
428
|
+
if detail:
|
|
429
|
+
return f"{type(exc).__name__}: {detail}"
|
|
430
|
+
return type(exc).__name__
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def _run_sync[T](factory: Callable[[], Coroutine[object, object, T]]) -> T:
|
|
434
|
+
"""Run an asynchronous provider operation from synchronous code.
|
|
435
|
+
|
|
436
|
+
Bridges an asynchronous coroutine factory to the synchronous
|
|
437
|
+
BaseSandbox interface.
|
|
438
|
+
|
|
439
|
+
When no event loop is running in the calling thread, the coroutine
|
|
440
|
+
is executed directly using `asyncio.run()`.
|
|
441
|
+
|
|
442
|
+
When an event loop is already running, execution is delegated to a
|
|
443
|
+
dedicated worker thread with its own event loop. This avoids attempting
|
|
444
|
+
to nest `asyncio.run()` inside an existing event loop.
|
|
445
|
+
|
|
446
|
+
The coroutine is created and executed in the same thread, ensuring
|
|
447
|
+
that coroutine-bound resources are not transferred between threads.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
factory: Zero-argument callable that creates the coroutine to execute.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
The result produced by the asynchronous operation.
|
|
454
|
+
|
|
455
|
+
Raises:
|
|
456
|
+
Exception: Any exception raised by the asynchronous operation is
|
|
457
|
+
propagated to the synchronous caller.
|
|
458
|
+
"""
|
|
459
|
+
try:
|
|
460
|
+
asyncio.get_running_loop()
|
|
461
|
+
except RuntimeError:
|
|
462
|
+
return asyncio.run(factory())
|
|
463
|
+
|
|
464
|
+
def run_in_thread() -> T:
|
|
465
|
+
return asyncio.run(factory())
|
|
466
|
+
|
|
467
|
+
with ThreadPoolExecutor(
|
|
468
|
+
max_workers=1, thread_name_prefix="microsandbox-sync"
|
|
469
|
+
) as executor:
|
|
470
|
+
future: Future[T] = executor.submit(run_in_thread)
|
|
471
|
+
return future.result()
|
|
File without changes
|