langchain-e2b 0.0.3__tar.gz → 0.0.4__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.
Files changed (22) hide show
  1. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/CHANGELOG.md +5 -0
  2. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/PKG-INFO +21 -4
  3. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/README.md +20 -3
  4. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/langchain_e2b/_version.py +1 -1
  5. langchain_e2b-0.0.4/langchain_e2b/provider.py +215 -0
  6. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/pyproject.toml +5 -1
  7. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/integration_tests/test_integration.py +42 -0
  8. langchain_e2b-0.0.4/tests/unit_tests/test_provider.py +310 -0
  9. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/uv.lock +1698 -57
  10. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/.github/workflows/ci.yml +0 -0
  11. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/.github/workflows/release.yml +0 -0
  12. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/.gitignore +0 -0
  13. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/LICENSE +0 -0
  14. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/Makefile +0 -0
  15. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/langchain_e2b/__init__.py +0 -0
  16. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/langchain_e2b/sandbox.py +0 -0
  17. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/__init__.py +0 -0
  18. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/integration_tests/__init__.py +0 -0
  19. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/test_import.py +0 -0
  20. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/unit_tests/__init__.py +0 -0
  21. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/unit_tests/test_import.py +0 -0
  22. {langchain_e2b-0.0.3 → langchain_e2b-0.0.4}/tests/unit_tests/test_sandbox.py +0 -0
@@ -2,6 +2,11 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## 0.0.4
6
+
7
+ - Added a Deep Agents Code sandbox provider entry point so `dcode --sandbox e2b`
8
+ works after installing `langchain-e2b` into the `dcode` environment.
9
+
5
10
  ## 0.0.3
6
11
 
7
12
  - Removed the `E2BProvider` lifecycle helper. Use the E2B SDK to create,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langchain-e2b
3
- Version: 0.0.3
3
+ Version: 0.0.4
4
4
  Summary: E2B sandbox integration for Deep Agents
5
5
  Project-URL: Homepage, https://github.com/e2b-dev/langchain-e2b
6
6
  Project-URL: Repository, https://github.com/e2b-dev/langchain-e2b
@@ -33,6 +33,8 @@ Description-Content-Type: text/markdown
33
33
  pip install langchain-e2b
34
34
  ```
35
35
 
36
+ ## Deep Agents SDK
37
+
36
38
  ```python
37
39
  from e2b import Sandbox
38
40
  from langchain_e2b import E2BSandbox
@@ -47,15 +49,30 @@ finally:
47
49
  e2b_sandbox.kill()
48
50
  ```
49
51
 
52
+ ## Deep Agents Code
53
+
54
+ Install `langchain-e2b` into the `dcode` environment, then run with the E2B
55
+ sandbox provider:
56
+
57
+ ```bash
58
+ dcode --install langchain-e2b --package
59
+ export E2B_API_KEY=...
60
+ dcode --sandbox e2b
61
+ ```
62
+
50
63
  ## What is this?
51
64
 
52
65
  `langchain-e2b` adapts an existing E2B sandbox to the Deep Agents sandbox
53
66
  protocol. It uses the low-level `e2b` SDK so Deep Agents can run shell commands
54
67
  and move files through the standard Deep Agents sandbox interface.
55
68
 
56
- This package intentionally does not hide E2B sandbox lifecycle management. Use
57
- the E2B SDK to create, connect to, configure, and kill sandboxes, then pass the
58
- connected sandbox object to `E2BSandbox`.
69
+ For SDK use, this package intentionally does not hide E2B sandbox lifecycle
70
+ management. Use the E2B SDK to create, connect to, configure, and kill
71
+ sandboxes, then pass the connected sandbox object to `E2BSandbox`.
72
+
73
+ For Deep Agents Code, the package also exposes a `dcode` sandbox provider entry
74
+ point. The provider creates, reconnects to, and deletes E2B sandboxes for
75
+ `dcode --sandbox e2b`.
59
76
 
60
77
  ## Contributing
61
78
 
@@ -10,6 +10,8 @@
10
10
  pip install langchain-e2b
11
11
  ```
12
12
 
13
+ ## Deep Agents SDK
14
+
13
15
  ```python
14
16
  from e2b import Sandbox
15
17
  from langchain_e2b import E2BSandbox
@@ -24,15 +26,30 @@ finally:
24
26
  e2b_sandbox.kill()
25
27
  ```
26
28
 
29
+ ## Deep Agents Code
30
+
31
+ Install `langchain-e2b` into the `dcode` environment, then run with the E2B
32
+ sandbox provider:
33
+
34
+ ```bash
35
+ dcode --install langchain-e2b --package
36
+ export E2B_API_KEY=...
37
+ dcode --sandbox e2b
38
+ ```
39
+
27
40
  ## What is this?
28
41
 
29
42
  `langchain-e2b` adapts an existing E2B sandbox to the Deep Agents sandbox
30
43
  protocol. It uses the low-level `e2b` SDK so Deep Agents can run shell commands
31
44
  and move files through the standard Deep Agents sandbox interface.
32
45
 
33
- This package intentionally does not hide E2B sandbox lifecycle management. Use
34
- the E2B SDK to create, connect to, configure, and kill sandboxes, then pass the
35
- connected sandbox object to `E2BSandbox`.
46
+ For SDK use, this package intentionally does not hide E2B sandbox lifecycle
47
+ management. Use the E2B SDK to create, connect to, configure, and kill
48
+ sandboxes, then pass the connected sandbox object to `E2BSandbox`.
49
+
50
+ For Deep Agents Code, the package also exposes a `dcode` sandbox provider entry
51
+ point. The provider creates, reconnects to, and deletes E2B sandboxes for
52
+ `dcode --sandbox e2b`.
36
53
 
37
54
  ## Contributing
38
55
 
@@ -2,4 +2,4 @@
2
2
 
3
3
  # Keep the `x-release-please-version` annotation — release-please uses it to
4
4
  # bump `__version__` in sync with `pyproject.toml` on every release PR.
5
- __version__ = "0.0.3" # x-release-please-version
5
+ __version__ = "0.0.4" # x-release-please-version
@@ -0,0 +1,215 @@
1
+ """E2B sandbox lifecycle provider for Deep Agents Code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from collections.abc import Callable
7
+ from typing import NoReturn
8
+
9
+ import e2b
10
+ from deepagents_code.integrations.sandbox_provider import (
11
+ SandboxInstallHint,
12
+ SandboxNotFoundError,
13
+ SandboxProvider,
14
+ SandboxProviderMetadata,
15
+ )
16
+
17
+ from langchain_e2b.sandbox import DEFAULT_WORKDIR, E2BSandbox
18
+
19
+ DEFAULT_SANDBOX_TIMEOUT = 30 * 60
20
+ DEFAULT_COMMAND_TIMEOUT = 30 * 60
21
+
22
+ EnvResolver = Callable[[str], str | None]
23
+
24
+
25
+ def _default_resolve_env_var(name: str) -> str | None:
26
+ if not name.startswith("DEEPAGENTS_CODE_"):
27
+ prefixed = f"DEEPAGENTS_CODE_{name}"
28
+ if prefixed in os.environ:
29
+ return os.environ[prefixed] or None
30
+ return os.environ.get(name) or None
31
+
32
+
33
+ def _resolve_optional_env_var(
34
+ resolve_env_var: EnvResolver,
35
+ name: str,
36
+ ) -> str | None:
37
+ value = resolve_env_var(name)
38
+ return value or None
39
+
40
+
41
+ def _resolve_int(
42
+ value: int | str | None,
43
+ *,
44
+ name: str,
45
+ default: int,
46
+ allow_zero: bool = False,
47
+ ) -> int:
48
+ if value is None:
49
+ return default
50
+
51
+ try:
52
+ resolved = int(value)
53
+ except (TypeError, ValueError) as exc:
54
+ msg = f"{name} must be an integer number of seconds"
55
+ raise ValueError(msg) from exc
56
+
57
+ lower_bound = 0 if allow_zero else 1
58
+ if resolved < lower_bound:
59
+ requirement = "non-negative" if allow_zero else "positive"
60
+ msg = f"{name} must be {requirement}"
61
+ raise ValueError(msg)
62
+ return resolved
63
+
64
+
65
+ def _raise_unsupported_kwargs(kwargs: dict[str, object]) -> NoReturn:
66
+ names = ", ".join(sorted(kwargs))
67
+ msg = f"Received unsupported arguments: {names}"
68
+ raise TypeError(msg)
69
+
70
+
71
+ class E2BProvider(SandboxProvider):
72
+ """Manage E2B sandboxes for Deep Agents Code."""
73
+
74
+ _metadata = SandboxProviderMetadata(
75
+ name="e2b",
76
+ working_dir=DEFAULT_WORKDIR,
77
+ install=SandboxInstallHint(kind="package", name="langchain-e2b"),
78
+ supports_sandbox_id=True,
79
+ supports_snapshot_name=False,
80
+ backend_module="langchain_e2b",
81
+ )
82
+
83
+ def __init__(
84
+ self,
85
+ *,
86
+ resolve_env_var: EnvResolver | None = None,
87
+ ) -> None:
88
+ """Initialize the provider without touching credentials.
89
+
90
+ Deep Agents Code may instantiate providers only to read metadata during
91
+ discovery, so credentials are resolved lazily by lifecycle methods.
92
+
93
+ Args:
94
+ resolve_env_var: Environment resolver used for provider settings.
95
+ """
96
+ self._resolve_env_var = resolve_env_var or _default_resolve_env_var
97
+
98
+ @property
99
+ def metadata(self) -> SandboxProviderMetadata:
100
+ """Return metadata used by Deep Agents Code provider discovery."""
101
+ return self._metadata
102
+
103
+ def get_or_create(
104
+ self,
105
+ *,
106
+ sandbox_id: str | None = None,
107
+ timeout: int | str | None = None,
108
+ template: str | None = None,
109
+ workdir: str = DEFAULT_WORKDIR,
110
+ command_timeout: int | str | None = None,
111
+ **kwargs: object,
112
+ ) -> E2BSandbox:
113
+ """Get or create an E2B sandbox backend.
114
+
115
+ Args:
116
+ sandbox_id: Existing E2B sandbox ID, or `None` to create one.
117
+ timeout: E2B sandbox lifetime in seconds.
118
+ template: E2B template for new sandboxes.
119
+ workdir: Working directory used by command execution.
120
+ command_timeout: Default command timeout for the backend.
121
+ **kwargs: Unsupported provider options.
122
+
123
+ Returns:
124
+ E2B sandbox backend.
125
+
126
+ Raises:
127
+ SandboxNotFoundError: If `sandbox_id` does not exist.
128
+ TypeError: If unsupported provider options are passed.
129
+ ValueError: If credentials or timeout settings are invalid.
130
+ """
131
+ if kwargs:
132
+ _raise_unsupported_kwargs(kwargs)
133
+
134
+ api_key = self._resolve_api_key()
135
+ sandbox_timeout = self._resolve_sandbox_timeout(timeout)
136
+ default_command_timeout = _resolve_int(
137
+ command_timeout,
138
+ name="command_timeout",
139
+ default=DEFAULT_COMMAND_TIMEOUT,
140
+ allow_zero=True,
141
+ )
142
+
143
+ try:
144
+ if sandbox_id is not None:
145
+ sandbox = e2b.Sandbox.connect(
146
+ sandbox_id,
147
+ timeout=sandbox_timeout,
148
+ api_key=api_key,
149
+ )
150
+ else:
151
+ resolved_template = template or _resolve_optional_env_var(
152
+ self._resolve_env_var,
153
+ "E2B_TEMPLATE",
154
+ )
155
+ if resolved_template:
156
+ sandbox = e2b.Sandbox.create(
157
+ template=resolved_template,
158
+ timeout=sandbox_timeout,
159
+ api_key=api_key,
160
+ )
161
+ else:
162
+ sandbox = e2b.Sandbox.create(
163
+ timeout=sandbox_timeout,
164
+ api_key=api_key,
165
+ )
166
+ except e2b.SandboxNotFoundException as exc:
167
+ if sandbox_id is None:
168
+ raise
169
+ raise SandboxNotFoundError(sandbox_id) from exc
170
+
171
+ return E2BSandbox(
172
+ sandbox=sandbox,
173
+ workdir=workdir,
174
+ timeout=default_command_timeout,
175
+ )
176
+
177
+ def delete(self, *, sandbox_id: str, **kwargs: object) -> None:
178
+ """Kill an E2B sandbox.
179
+
180
+ Args:
181
+ sandbox_id: E2B sandbox ID.
182
+ **kwargs: Unsupported provider options.
183
+
184
+ Raises:
185
+ SandboxNotFoundError: If `sandbox_id` does not exist.
186
+ TypeError: If unsupported provider options are passed.
187
+ ValueError: If credentials are missing.
188
+ """
189
+ if kwargs:
190
+ _raise_unsupported_kwargs(kwargs)
191
+ try:
192
+ e2b.Sandbox.kill(sandbox_id, api_key=self._resolve_api_key())
193
+ except e2b.SandboxNotFoundException as exc:
194
+ raise SandboxNotFoundError(sandbox_id) from exc
195
+
196
+ def _resolve_api_key(self) -> str:
197
+ api_key = _resolve_optional_env_var(self._resolve_env_var, "E2B_API_KEY")
198
+ if not api_key:
199
+ msg = (
200
+ "No E2B API key found. Set E2B_API_KEY or DEEPAGENTS_CODE_E2B_API_KEY."
201
+ )
202
+ raise ValueError(msg)
203
+ return api_key
204
+
205
+ def _resolve_sandbox_timeout(self, timeout: int | str | None) -> int:
206
+ env_timeout = (
207
+ None
208
+ if timeout is not None
209
+ else _resolve_optional_env_var(self._resolve_env_var, "E2B_SANDBOX_TIMEOUT")
210
+ )
211
+ return _resolve_int(
212
+ timeout if timeout is not None else env_timeout,
213
+ name="timeout",
214
+ default=DEFAULT_SANDBOX_TIMEOUT,
215
+ )
@@ -19,13 +19,16 @@ classifiers = [
19
19
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
20
20
  ]
21
21
 
22
- version = "0.0.3"
22
+ version = "0.0.4"
23
23
  requires-python = ">=3.11,<4.0"
24
24
  dependencies = [
25
25
  "deepagents>=0.6.0,<0.7.0",
26
26
  "e2b>=2.25.1,<3.0.0",
27
27
  ]
28
28
 
29
+ [project.entry-points."deepagents_code.sandbox_providers"]
30
+ e2b = "langchain_e2b.provider:E2BProvider"
31
+
29
32
  [tool.hatch.build.targets.wheel]
30
33
  packages = ["langchain_e2b"]
31
34
 
@@ -46,6 +49,7 @@ test = [
46
49
  "ruff>=0.13.1,<0.16.0",
47
50
  "ty>=0.0.1,<1.0.0",
48
51
  "langchain-tests>=1.1.6",
52
+ "deepagents-code>=0.1.19,<0.2.0",
49
53
  ]
50
54
 
51
55
  [tool.uv]
@@ -11,6 +11,7 @@ from e2b.exceptions import SandboxException
11
11
  from langchain_tests.integration_tests import SandboxIntegrationTests
12
12
 
13
13
  from langchain_e2b import E2BSandbox
14
+ from langchain_e2b.provider import E2BProvider
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from collections.abc import Iterator
@@ -47,6 +48,31 @@ class TestE2BSandboxStandard(SandboxIntegrationTests):
47
48
  _kill_sandbox(sandbox)
48
49
 
49
50
 
51
+ def test_e2b_provider_creates_executes_and_deletes_sandbox() -> None:
52
+ api_key = os.environ.get("E2B_API_KEY")
53
+ if not api_key:
54
+ pytest.skip(
55
+ "Missing secrets for E2B provider integration test: set E2B_API_KEY"
56
+ )
57
+
58
+ provider = E2BProvider()
59
+ sandbox_id: str | None = None
60
+ try:
61
+ backend = provider.get_or_create(
62
+ timeout=60 * 60,
63
+ template=os.environ.get("E2B_TEMPLATE"),
64
+ command_timeout=30,
65
+ )
66
+ sandbox_id = backend.id
67
+ result = backend.execute("echo provider-ready", timeout=10)
68
+
69
+ assert result.exit_code == 0
70
+ assert "provider-ready" in result.output
71
+ finally:
72
+ if sandbox_id is not None:
73
+ _delete_provider_sandbox(provider, sandbox_id)
74
+
75
+
50
76
  def _kill_sandbox(sandbox: Sandbox) -> None:
51
77
  last_error: BaseException | None = None
52
78
  for attempt in range(KILL_ATTEMPTS):
@@ -61,3 +87,19 @@ def _kill_sandbox(sandbox: Sandbox) -> None:
61
87
 
62
88
  msg = f"Failed to kill E2B sandbox {sandbox.sandbox_id!r}"
63
89
  raise RuntimeError(msg) from last_error
90
+
91
+
92
+ def _delete_provider_sandbox(provider: E2BProvider, sandbox_id: str) -> None:
93
+ last_error: BaseException | None = None
94
+ for attempt in range(KILL_ATTEMPTS):
95
+ try:
96
+ provider.delete(sandbox_id=sandbox_id)
97
+ except (httpx.HTTPError, SandboxException) as exc:
98
+ last_error = exc
99
+ if attempt + 1 < KILL_ATTEMPTS:
100
+ time.sleep(KILL_RETRY_DELAY_SECONDS)
101
+ else:
102
+ return
103
+
104
+ msg = f"Failed to delete E2B sandbox {sandbox_id!r}"
105
+ raise RuntimeError(msg) from last_error