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.
@@ -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()