zjcode 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. deepagents_code/__init__.py +42 -0
  2. deepagents_code/__main__.py +6 -0
  3. deepagents_code/_ask_user_types.py +90 -0
  4. deepagents_code/_cli_context.py +96 -0
  5. deepagents_code/_constants.py +41 -0
  6. deepagents_code/_debug.py +148 -0
  7. deepagents_code/_debug_buffer.py +204 -0
  8. deepagents_code/_env_vars.py +411 -0
  9. deepagents_code/_fake_models.py +66 -0
  10. deepagents_code/_git.py +521 -0
  11. deepagents_code/_paths.py +69 -0
  12. deepagents_code/_server_config.py +576 -0
  13. deepagents_code/_session_stats.py +235 -0
  14. deepagents_code/_startup_error.py +45 -0
  15. deepagents_code/_testing_models.py +305 -0
  16. deepagents_code/_textual_patches.py +420 -0
  17. deepagents_code/_tool_stream.py +694 -0
  18. deepagents_code/_version.py +46 -0
  19. deepagents_code/agent.py +1976 -0
  20. deepagents_code/app.py +19239 -0
  21. deepagents_code/app.tcss +438 -0
  22. deepagents_code/approval_mode.py +131 -0
  23. deepagents_code/ask_user.py +306 -0
  24. deepagents_code/auth_display.py +185 -0
  25. deepagents_code/auth_store.py +545 -0
  26. deepagents_code/built_in_skills/__init__.py +5 -0
  27. deepagents_code/built_in_skills/remember/SKILL.md +118 -0
  28. deepagents_code/built_in_skills/skill-creator/SKILL.md +383 -0
  29. deepagents_code/built_in_skills/skill-creator/scripts/init_skill.py +366 -0
  30. deepagents_code/built_in_skills/skill-creator/scripts/quick_validate.py +158 -0
  31. deepagents_code/client/__init__.py +1 -0
  32. deepagents_code/client/commands/__init__.py +1 -0
  33. deepagents_code/client/commands/auth.py +541 -0
  34. deepagents_code/client/commands/config.py +714 -0
  35. deepagents_code/client/commands/mcp.py +250 -0
  36. deepagents_code/client/commands/tools.py +416 -0
  37. deepagents_code/client/launch/__init__.py +1 -0
  38. deepagents_code/client/launch/server.py +978 -0
  39. deepagents_code/client/launch/server_manager.py +540 -0
  40. deepagents_code/client/non_interactive.py +1758 -0
  41. deepagents_code/client/remote_client.py +794 -0
  42. deepagents_code/clipboard.py +217 -0
  43. deepagents_code/command_registry.py +450 -0
  44. deepagents_code/config.py +4829 -0
  45. deepagents_code/config_manifest.py +1451 -0
  46. deepagents_code/configurable_model.py +577 -0
  47. deepagents_code/default_agent_prompt.md +12 -0
  48. deepagents_code/doctor.py +559 -0
  49. deepagents_code/editor.py +142 -0
  50. deepagents_code/event_bus.py +411 -0
  51. deepagents_code/extras_info.py +661 -0
  52. deepagents_code/file_ops.py +576 -0
  53. deepagents_code/formatting.py +135 -0
  54. deepagents_code/goal_rubric.py +497 -0
  55. deepagents_code/goal_tools.py +459 -0
  56. deepagents_code/hooks.py +348 -0
  57. deepagents_code/input.py +1041 -0
  58. deepagents_code/integrations/__init__.py +1 -0
  59. deepagents_code/integrations/openai_codex.py +551 -0
  60. deepagents_code/integrations/sandbox_config.py +198 -0
  61. deepagents_code/integrations/sandbox_factory.py +1124 -0
  62. deepagents_code/integrations/sandbox_provider.py +137 -0
  63. deepagents_code/integrations/sandbox_registry.py +350 -0
  64. deepagents_code/iterm_cursor_guide.py +176 -0
  65. deepagents_code/local_context.py +926 -0
  66. deepagents_code/main.py +3985 -0
  67. deepagents_code/managed_tools.py +642 -0
  68. deepagents_code/mcp_auth.py +1950 -0
  69. deepagents_code/mcp_config.py +176 -0
  70. deepagents_code/mcp_disabled.py +212 -0
  71. deepagents_code/mcp_login_service.py +281 -0
  72. deepagents_code/mcp_oauth_ui.py +199 -0
  73. deepagents_code/mcp_providers/__init__.py +23 -0
  74. deepagents_code/mcp_providers/_registry.py +39 -0
  75. deepagents_code/mcp_providers/base.py +133 -0
  76. deepagents_code/mcp_providers/github.py +102 -0
  77. deepagents_code/mcp_providers/slack.py +175 -0
  78. deepagents_code/mcp_tools.py +2427 -0
  79. deepagents_code/mcp_trust.py +207 -0
  80. deepagents_code/media_utils.py +826 -0
  81. deepagents_code/memory_guard.py +474 -0
  82. deepagents_code/model_config.py +4156 -0
  83. deepagents_code/notifications.py +247 -0
  84. deepagents_code/offload.py +402 -0
  85. deepagents_code/onboarding.py +223 -0
  86. deepagents_code/output.py +69 -0
  87. deepagents_code/paste_collapse.py +103 -0
  88. deepagents_code/project_utils.py +231 -0
  89. deepagents_code/py.typed +0 -0
  90. deepagents_code/reasoning_effort.py +641 -0
  91. deepagents_code/reliable_rubric.py +97 -0
  92. deepagents_code/resume_state.py +237 -0
  93. deepagents_code/server_graph.py +310 -0
  94. deepagents_code/session_end_summary.py +329 -0
  95. deepagents_code/sessions.py +1716 -0
  96. deepagents_code/skills/__init__.py +18 -0
  97. deepagents_code/skills/commands.py +1252 -0
  98. deepagents_code/skills/invocation.py +112 -0
  99. deepagents_code/skills/load.py +196 -0
  100. deepagents_code/skills/trust.py +547 -0
  101. deepagents_code/state_migration.py +136 -0
  102. deepagents_code/subagents.py +278 -0
  103. deepagents_code/system_prompt.md +204 -0
  104. deepagents_code/terminal_capabilities.py +115 -0
  105. deepagents_code/terminal_escape.py +287 -0
  106. deepagents_code/theme.py +891 -0
  107. deepagents_code/todo_list_prompt.md +12 -0
  108. deepagents_code/tool_catalog.py +509 -0
  109. deepagents_code/tool_display.py +367 -0
  110. deepagents_code/tools.py +516 -0
  111. deepagents_code/tui/__init__.py +1 -0
  112. deepagents_code/tui/textual_adapter.py +2553 -0
  113. deepagents_code/tui/widgets/__init__.py +9 -0
  114. deepagents_code/tui/widgets/_js_eval_display.py +139 -0
  115. deepagents_code/tui/widgets/_links.py +261 -0
  116. deepagents_code/tui/widgets/agent_selector.py +420 -0
  117. deepagents_code/tui/widgets/approval.py +602 -0
  118. deepagents_code/tui/widgets/ask_user.py +515 -0
  119. deepagents_code/tui/widgets/auth.py +1997 -0
  120. deepagents_code/tui/widgets/autocomplete.py +890 -0
  121. deepagents_code/tui/widgets/chat_input.py +3181 -0
  122. deepagents_code/tui/widgets/codex_auth.py +452 -0
  123. deepagents_code/tui/widgets/cwd_switch.py +242 -0
  124. deepagents_code/tui/widgets/debug_console.py +868 -0
  125. deepagents_code/tui/widgets/diff.py +252 -0
  126. deepagents_code/tui/widgets/effort_selector.py +189 -0
  127. deepagents_code/tui/widgets/goal_review.py +390 -0
  128. deepagents_code/tui/widgets/goal_status.py +50 -0
  129. deepagents_code/tui/widgets/history.py +194 -0
  130. deepagents_code/tui/widgets/install_confirm.py +248 -0
  131. deepagents_code/tui/widgets/launch_init.py +482 -0
  132. deepagents_code/tui/widgets/loading.py +227 -0
  133. deepagents_code/tui/widgets/mcp_login.py +539 -0
  134. deepagents_code/tui/widgets/mcp_reconnect.py +212 -0
  135. deepagents_code/tui/widgets/mcp_viewer.py +1634 -0
  136. deepagents_code/tui/widgets/message_store.py +999 -0
  137. deepagents_code/tui/widgets/messages.py +3846 -0
  138. deepagents_code/tui/widgets/model_selector.py +2343 -0
  139. deepagents_code/tui/widgets/notification_center.py +456 -0
  140. deepagents_code/tui/widgets/notification_detail.py +257 -0
  141. deepagents_code/tui/widgets/notification_settings.py +170 -0
  142. deepagents_code/tui/widgets/restart_prompt.py +158 -0
  143. deepagents_code/tui/widgets/skill_trust.py +131 -0
  144. deepagents_code/tui/widgets/startup_tip.py +91 -0
  145. deepagents_code/tui/widgets/status.py +781 -0
  146. deepagents_code/tui/widgets/subagent_panel.py +969 -0
  147. deepagents_code/tui/widgets/theme_selector.py +401 -0
  148. deepagents_code/tui/widgets/thread_selector.py +2564 -0
  149. deepagents_code/tui/widgets/tool_renderers.py +190 -0
  150. deepagents_code/tui/widgets/tool_widgets.py +304 -0
  151. deepagents_code/tui/widgets/update_available.py +311 -0
  152. deepagents_code/tui/widgets/update_confirm.py +184 -0
  153. deepagents_code/tui/widgets/update_progress.py +327 -0
  154. deepagents_code/tui/widgets/welcome.py +508 -0
  155. deepagents_code/turn_end_summary.py +522 -0
  156. deepagents_code/ui.py +858 -0
  157. deepagents_code/unicode_security.py +563 -0
  158. deepagents_code/update_check.py +3124 -0
  159. zjcode-0.0.1.data/data/deepagents_code/default_agent_prompt.md +12 -0
  160. zjcode-0.0.1.dist-info/METADATA +220 -0
  161. zjcode-0.0.1.dist-info/RECORD +163 -0
  162. zjcode-0.0.1.dist-info/WHEEL +4 -0
  163. zjcode-0.0.1.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,1124 @@
1
+ """Sandbox lifecycle management with provider abstraction."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import importlib
7
+ import importlib.util
8
+ import logging
9
+ import os
10
+ import shlex
11
+ import string
12
+ import time
13
+ from contextlib import contextmanager
14
+ from datetime import timedelta
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING, Any, Literal, Protocol
17
+
18
+ from rich.markup import escape as escape_markup
19
+
20
+ from deepagents_code.config import console, get_glyphs
21
+ from deepagents_code.integrations.sandbox_provider import (
22
+ SandboxNotFoundError,
23
+ SandboxProvider,
24
+ )
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Generator
30
+ from types import ModuleType
31
+
32
+ from deepagents.backends.protocol import SandboxBackendProtocol
33
+
34
+ from deepagents_code.integrations.sandbox_registry import SandboxRegistry
35
+
36
+
37
+ def _run_sandbox_setup(backend: SandboxBackendProtocol, setup_script_path: str) -> None:
38
+ """Run users setup script in sandbox with env var expansion.
39
+
40
+ Args:
41
+ backend: Sandbox backend instance
42
+ setup_script_path: Path to setup script file
43
+
44
+ Raises:
45
+ FileNotFoundError: If the setup script does not exist.
46
+ RuntimeError: If the setup script fails to execute.
47
+ """
48
+ script_path = Path(setup_script_path)
49
+ if not script_path.exists():
50
+ msg = f"Setup script not found: {setup_script_path}"
51
+ raise FileNotFoundError(msg)
52
+
53
+ console.print(
54
+ f"[dim]Running setup script: {escape_markup(setup_script_path)}...[/dim]"
55
+ )
56
+
57
+ # Read script content
58
+ script_content = script_path.read_text(encoding="utf-8")
59
+
60
+ # Expand ${VAR} syntax using local environment
61
+ template = string.Template(script_content)
62
+ expanded_script = template.safe_substitute(os.environ)
63
+
64
+ # Execute expanded script in sandbox
65
+ result = backend.execute(f"bash -c {shlex.quote(expanded_script)}")
66
+
67
+ if result.exit_code != 0:
68
+ console.print(f"[red]Setup script failed (exit {result.exit_code}):[/red]")
69
+ console.print(f"[dim]{escape_markup(result.output)}[/dim]")
70
+ msg = "Setup failed - aborting"
71
+ raise RuntimeError(msg)
72
+
73
+ console.print(f"[green]{get_glyphs().checkmark} Setup complete[/green]")
74
+
75
+
76
+ @contextmanager
77
+ def create_sandbox(
78
+ provider: str,
79
+ *,
80
+ sandbox_id: str | None = None,
81
+ snapshot_name: str | None = None,
82
+ setup_script_path: str | None = None,
83
+ params: dict[str, Any] | None = None,
84
+ ) -> Generator[SandboxBackendProtocol, None, None]:
85
+ """Create or connect to a sandbox of the specified provider.
86
+
87
+ This is the unified interface for sandbox creation using the
88
+ provider abstraction.
89
+
90
+ Args:
91
+ provider: Sandbox provider name. Built-ins (`'agentcore'`, `'daytona'`,
92
+ `'langsmith'`, `'modal'`, `'runloop'`, `'vercel'`), entry-point
93
+ providers, and config-declared providers are all resolved through
94
+ the registry.
95
+ sandbox_id: Optional existing sandbox ID to reuse
96
+ snapshot_name: Optional sandbox snapshot name to use or create.
97
+ Honored by providers whose metadata sets `supports_snapshot_name`
98
+ (built-ins: `'langsmith'` snapshot, `'runloop'` blueprint); must be
99
+ `None` for other providers.
100
+ setup_script_path: Optional path to setup script to run after sandbox starts
101
+ params: Extra keyword arguments forwarded to `provider.get_or_create()`
102
+ (e.g. config-declared `[sandboxes.providers.<name>.params]`).
103
+
104
+ Yields:
105
+ `SandboxBackendProtocol` instance
106
+
107
+ Raises:
108
+ ValueError: If `snapshot_name` is provided for an unsupported provider,
109
+ or combined with `sandbox_id` (snapshots only apply to fresh sandboxes).
110
+ """
111
+ registry = _get_registry()
112
+ metadata = registry.get_metadata(provider)
113
+ if snapshot_name is not None and (
114
+ metadata is None or not metadata.supports_snapshot_name
115
+ ):
116
+ msg = (
117
+ f"snapshot_name is not supported by provider {provider!r} "
118
+ f"(got snapshot_name={snapshot_name!r})"
119
+ )
120
+ raise ValueError(msg)
121
+ if snapshot_name is not None and sandbox_id is not None:
122
+ msg = (
123
+ "snapshot_name cannot be combined with sandbox_id; "
124
+ "snapshots only apply when creating a fresh sandbox"
125
+ )
126
+ raise ValueError(msg)
127
+
128
+ # Get provider instance (reuse the registry already built above so we
129
+ # don't re-read the config file and re-scan entry points).
130
+ provider_obj = _get_provider(provider, registry=registry)
131
+
132
+ # Determine if we should cleanup (only cleanup if we created it)
133
+ should_cleanup = sandbox_id is None
134
+ provider_kwargs: dict[str, Any] = dict(registry.get_params(provider))
135
+ if params:
136
+ provider_kwargs.update(params)
137
+ if snapshot_name is not None:
138
+ provider_kwargs["snapshot"] = snapshot_name
139
+
140
+ # Create or connect to sandbox
141
+ console.print(f"[yellow]Starting {provider} sandbox...[/yellow]")
142
+ backend = provider_obj.get_or_create(sandbox_id=sandbox_id, **provider_kwargs)
143
+ glyphs = get_glyphs()
144
+ console.print(
145
+ f"[green]{glyphs.checkmark} {provider.capitalize()} sandbox ready: "
146
+ f"{backend.id}[/green]"
147
+ )
148
+
149
+ # Run setup script if provided
150
+ if setup_script_path:
151
+ _run_sandbox_setup(backend, setup_script_path)
152
+
153
+ try:
154
+ yield backend
155
+ finally:
156
+ if should_cleanup:
157
+ try:
158
+ console.print(
159
+ f"[dim]Terminating {provider} sandbox {backend.id}...[/dim]"
160
+ )
161
+ provider_obj.delete(sandbox_id=backend.id)
162
+ glyphs = get_glyphs()
163
+ console.print(
164
+ f"[dim]{glyphs.checkmark} {provider.capitalize()} sandbox "
165
+ f"{backend.id} terminated[/dim]"
166
+ )
167
+ except Exception as e: # noqa: BLE001 # Cleanup errors should not mask the original sandbox failure
168
+ warning = get_glyphs().warning
169
+ console.print(
170
+ f"[yellow]{warning} Cleanup failed for {provider} sandbox "
171
+ f"{backend.id}: {e}[/yellow]"
172
+ )
173
+
174
+
175
+ def _get_registry() -> SandboxRegistry:
176
+ """Build a `SandboxRegistry` from the current user config.
177
+
178
+ Not cached: each call re-reads the config file and re-scans entry points so
179
+ the registry reflects the latest state. Reuse a single instance within one
180
+ operation (see `create_sandbox`) rather than calling this repeatedly.
181
+
182
+ Returns:
183
+ A fresh `SandboxRegistry`.
184
+ """
185
+ from deepagents_code.integrations.sandbox_registry import SandboxRegistry
186
+
187
+ return SandboxRegistry.load()
188
+
189
+
190
+ def get_default_working_dir(provider: str) -> str:
191
+ """Get the default working directory for a given sandbox provider.
192
+
193
+ Args:
194
+ provider: Sandbox provider name. Resolved through the registry so
195
+ built-in, entry-point, and config providers are all supported.
196
+
197
+ Returns:
198
+ Default working directory path as string
199
+
200
+ Raises:
201
+ ValueError: If provider is unknown
202
+ """
203
+ metadata = _get_registry().get_metadata(provider)
204
+ if metadata is None:
205
+ msg = f"Unknown sandbox provider: {provider}"
206
+ raise ValueError(msg)
207
+ return metadata.working_dir
208
+
209
+
210
+ # ---------------------------------------------------------------------------
211
+ # Provider implementations
212
+ # ---------------------------------------------------------------------------
213
+
214
+
215
+ def _import_provider_module(
216
+ module_name: str,
217
+ *,
218
+ provider: str,
219
+ package: str,
220
+ ) -> ModuleType:
221
+ """Import an optional provider module with a provider-specific error message.
222
+
223
+ Args:
224
+ module_name: Python module name to import.
225
+ provider: Sandbox provider name (e.g. `'daytona'`).
226
+ package: PyPI package name exposed by the package extra.
227
+
228
+ Returns:
229
+ The imported module object.
230
+
231
+ Raises:
232
+ ImportError: If the optional dependency is not installed.
233
+ """
234
+ try:
235
+ return importlib.import_module(module_name)
236
+ except ImportError as exc:
237
+ msg = (
238
+ f"The '{provider}' sandbox provider requires the '{package}' package. "
239
+ f"Install it with: /install {provider} (in-app) or "
240
+ f"dcode --install {provider} (CLI)"
241
+ )
242
+ raise ImportError(msg) from exc
243
+
244
+
245
+ _LANGSMITH_DEFAULT_SNAPSHOT = "deepagents-code"
246
+ """Default LangSmith sandbox snapshot name used when none is specified."""
247
+
248
+ _LANGSMITH_DEFAULT_IMAGE = "python:3"
249
+ """Default Docker image for LangSmith sandbox snapshots when none is provided."""
250
+
251
+ _LANGSMITH_DEFAULT_FS_CAPACITY_BYTES = 16 * 1024**3
252
+ """Default filesystem capacity (16 GiB) for LangSmith sandbox snapshots."""
253
+
254
+
255
+ class _LangSmithProvider(SandboxProvider):
256
+ """LangSmith sandbox provider implementation.
257
+
258
+ Manages LangSmith sandbox lifecycle using the LangSmith SDK, booting
259
+ sandboxes from snapshots built from a Docker image.
260
+ """
261
+
262
+ def __init__(self, api_key: str | None = None) -> None:
263
+ """Initialize LangSmith provider.
264
+
265
+ Args:
266
+ api_key: LangSmith API key (defaults to `LANGSMITH_SANDBOX_API_KEY`,
267
+ then `LANGSMITH_API_KEY` env var).
268
+
269
+ Raises:
270
+ ValueError: If no LangSmith API key is found.
271
+ """
272
+ from langsmith.sandbox import SandboxClient
273
+
274
+ from deepagents_code.model_config import resolve_env_var
275
+
276
+ sandbox_key = resolve_env_var("LANGSMITH_SANDBOX_API_KEY")
277
+ if sandbox_key:
278
+ logger.debug("Using LangSmith API key from LANGSMITH_SANDBOX_API_KEY")
279
+ self._api_key: str | None = (
280
+ api_key
281
+ or sandbox_key
282
+ or resolve_env_var("LANGSMITH_API_KEY")
283
+ or resolve_env_var("LANGCHAIN_API_KEY")
284
+ )
285
+ if not self._api_key:
286
+ msg = (
287
+ "No LangSmith sandbox API key found. Set "
288
+ "LANGSMITH_API_KEY, LANGCHAIN_API_KEY, or LANGSMITH_SANDBOX_API_KEY "
289
+ "(or the DEEPAGENTS_CODE_-prefixed equivalents)."
290
+ )
291
+ raise ValueError(msg)
292
+ self._client: SandboxClient = SandboxClient(api_key=self._api_key)
293
+
294
+ def get_or_create(
295
+ self,
296
+ *,
297
+ sandbox_id: str | None = None,
298
+ timeout: int = 180,
299
+ snapshot: str | None = None,
300
+ snapshot_image: str | None = None,
301
+ fs_capacity_bytes: int | None = None,
302
+ **kwargs: Any,
303
+ ) -> SandboxBackendProtocol:
304
+ """Get existing or create new LangSmith sandbox.
305
+
306
+ Args:
307
+ sandbox_id: Optional existing sandbox name to reuse.
308
+ timeout: Timeout in seconds for sandbox startup.
309
+ snapshot: Snapshot name to boot from.
310
+
311
+ Resolved to a snapshot ID, creating the snapshot from
312
+ `snapshot_image` if missing. Overrides
313
+ `LANGSMITH_SANDBOX_SNAPSHOT_NAME`; overridden by
314
+ `LANGSMITH_SANDBOX_SNAPSHOT_ID` (ID, wins over everything).
315
+ snapshot_image: Docker image used when building the snapshot.
316
+ fs_capacity_bytes: Filesystem capacity when building the snapshot.
317
+ **kwargs: Additional LangSmith-specific parameters.
318
+
319
+ Returns:
320
+ `LangSmithSandbox` instance.
321
+
322
+ Raises:
323
+ RuntimeError: If sandbox connection or startup fails.
324
+ TypeError: If unsupported keyword arguments are provided.
325
+ """
326
+ from deepagents.backends.langsmith import LangSmithSandbox
327
+
328
+ from deepagents_code.model_config import resolve_env_var
329
+
330
+ if kwargs:
331
+ msg = f"Received unsupported arguments: {list(kwargs.keys())}"
332
+ raise TypeError(msg)
333
+ if sandbox_id:
334
+ # Connect to existing sandbox by name
335
+ try:
336
+ sandbox = self._client.get_sandbox(name=sandbox_id)
337
+ except Exception as e:
338
+ msg = f"Failed to connect to existing sandbox '{sandbox_id}': {e}"
339
+ raise RuntimeError(msg) from e
340
+ return LangSmithSandbox(sandbox)
341
+
342
+ # Explicit snapshot ID wins — skip name lookup and auto-build.
343
+ env_snapshot_id = resolve_env_var("LANGSMITH_SANDBOX_SNAPSHOT_ID")
344
+ if env_snapshot_id:
345
+ snapshot_id = env_snapshot_id
346
+ snapshot_name = env_snapshot_id
347
+ else:
348
+ env_snapshot_name = resolve_env_var("LANGSMITH_SANDBOX_SNAPSHOT_NAME")
349
+ snapshot_name = snapshot or env_snapshot_name or _LANGSMITH_DEFAULT_SNAPSHOT
350
+ image = snapshot_image or _LANGSMITH_DEFAULT_IMAGE
351
+ capacity = fs_capacity_bytes or _LANGSMITH_DEFAULT_FS_CAPACITY_BYTES
352
+ snapshot_id = self._ensure_snapshot(snapshot_name, image, capacity)
353
+
354
+ try:
355
+ sandbox = self._client.create_sandbox(
356
+ snapshot_id=snapshot_id, timeout=timeout
357
+ )
358
+ except Exception as e:
359
+ msg = f"Failed to create sandbox from snapshot '{snapshot_name}': {e}"
360
+ raise RuntimeError(msg) from e
361
+
362
+ # Verify sandbox is ready by polling
363
+ for _ in range(timeout // 2):
364
+ try:
365
+ result = sandbox.run("echo ready", timeout=5)
366
+ if result.exit_code == 0:
367
+ break
368
+ except Exception: # noqa: S110, BLE001 # Sandbox not ready yet, continue polling
369
+ pass
370
+ time.sleep(2)
371
+ else:
372
+ # Cleanup on failure
373
+ with contextlib.suppress(Exception):
374
+ self._client.delete_sandbox(sandbox.name)
375
+ msg = f"LangSmith sandbox failed to start within {timeout} seconds"
376
+ raise RuntimeError(msg)
377
+
378
+ return LangSmithSandbox(sandbox)
379
+
380
+ def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002 # Required by SandboxFactory interface
381
+ """Delete a LangSmith sandbox.
382
+
383
+ Args:
384
+ sandbox_id: Sandbox name to delete
385
+ **kwargs: Additional parameters
386
+ """
387
+ self._client.delete_sandbox(sandbox_id)
388
+
389
+ def _ensure_snapshot(
390
+ self,
391
+ snapshot_name: str,
392
+ image: str,
393
+ fs_capacity_bytes: int,
394
+ ) -> str:
395
+ """Resolve a snapshot by name, building it from `image` if missing.
396
+
397
+ The LangSmith API exposes snapshots by ID, so we list and filter by
398
+ name. Only snapshots with `status == "ready"` are returned; a
399
+ matching-name snapshot in a non-ready state (`"building"`,
400
+ `"failed"`, etc.) raises rather than triggering a duplicate build,
401
+ which would mask the in-flight/failed snapshot.
402
+
403
+ When no matching snapshot exists at all, we build one with
404
+ `create_snapshot`, which blocks until the snapshot is ready.
405
+
406
+ Returns:
407
+ The snapshot ID ready to be passed to `create_sandbox`.
408
+
409
+ Raises:
410
+ RuntimeError: If listing or building the snapshot fails, or if
411
+ a matching-name snapshot exists but is not ready.
412
+ """
413
+ try:
414
+ snapshots = self._client.list_snapshots()
415
+ except Exception as e:
416
+ msg = f"Failed to list snapshots: {e}"
417
+ raise RuntimeError(msg) from e
418
+
419
+ non_ready_status: str | None = None
420
+ for snap in snapshots:
421
+ if snap.name != snapshot_name:
422
+ continue
423
+ if snap.status == "ready":
424
+ return snap.id
425
+ non_ready_status = snap.status
426
+
427
+ if non_ready_status is not None:
428
+ msg = (
429
+ f"Snapshot '{snapshot_name}' exists but is in state "
430
+ f"'{non_ready_status}'. Wait for it to finish building, or "
431
+ f"delete it to rebuild."
432
+ )
433
+ raise RuntimeError(msg)
434
+
435
+ try:
436
+ snapshot = self._client.create_snapshot(
437
+ name=snapshot_name,
438
+ docker_image=image,
439
+ fs_capacity_bytes=fs_capacity_bytes,
440
+ )
441
+ except Exception as create_err:
442
+ msg = f"Failed to build snapshot '{snapshot_name}': {create_err}"
443
+ raise RuntimeError(msg) from create_err
444
+ return snapshot.id
445
+
446
+
447
+ class _DaytonaProvider(SandboxProvider):
448
+ """Daytona sandbox provider — lifecycle management for Daytona sandboxes."""
449
+
450
+ def __init__(self) -> None:
451
+ daytona_module = _import_provider_module(
452
+ "daytona",
453
+ provider="daytona",
454
+ package="langchain-daytona",
455
+ )
456
+
457
+ from deepagents_code.model_config import resolve_env_var
458
+
459
+ api_key = resolve_env_var("DAYTONA_API_KEY")
460
+ if not api_key:
461
+ msg = (
462
+ "No Daytona API key found. Set DAYTONA_API_KEY "
463
+ "or DEEPAGENTS_CODE_DAYTONA_API_KEY."
464
+ )
465
+ raise ValueError(msg)
466
+ self._client = daytona_module.Daytona(
467
+ daytona_module.DaytonaConfig(
468
+ api_key=api_key,
469
+ api_url=resolve_env_var("DAYTONA_API_URL"),
470
+ )
471
+ )
472
+
473
+ def get_or_create(
474
+ self,
475
+ *,
476
+ sandbox_id: str | None = None,
477
+ timeout: int = 180,
478
+ **kwargs: Any, # noqa: ARG002
479
+ ) -> SandboxBackendProtocol:
480
+ """Get or create a Daytona sandbox.
481
+
482
+ Args:
483
+ sandbox_id: Not supported yet — must be None.
484
+ timeout: Seconds to wait for startup.
485
+ **kwargs: Unused.
486
+
487
+ Returns:
488
+ `DaytonaSandbox` instance.
489
+
490
+ Raises:
491
+ NotImplementedError: If `sandbox_id` is provided.
492
+ RuntimeError: If the sandbox fails to start.
493
+ """
494
+ daytona_backend = _import_provider_module(
495
+ "langchain_daytona",
496
+ provider="daytona",
497
+ package="langchain-daytona",
498
+ )
499
+
500
+ if sandbox_id:
501
+ msg = (
502
+ "Connecting to existing Daytona sandbox by ID not yet supported. "
503
+ "Create a new sandbox by omitting sandbox_id parameter."
504
+ )
505
+ raise NotImplementedError(msg)
506
+
507
+ sandbox = self._client.create()
508
+ last_exc: Exception | None = None
509
+ for _ in range(timeout // 2):
510
+ try:
511
+ result = sandbox.process.exec("echo ready", timeout=5)
512
+ if result.exit_code == 0:
513
+ break
514
+ except Exception as exc: # noqa: BLE001 # Transient failures expected during readiness polling
515
+ last_exc = exc
516
+ time.sleep(2)
517
+ else:
518
+ with contextlib.suppress(Exception): # Best-effort cleanup
519
+ sandbox.delete()
520
+ detail = f" Last error: {last_exc}" if last_exc else ""
521
+ msg = f"Daytona sandbox failed to start within {timeout} seconds.{detail}"
522
+ raise RuntimeError(msg)
523
+
524
+ return daytona_backend.DaytonaSandbox(sandbox=sandbox)
525
+
526
+ def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002
527
+ """Delete a Daytona sandbox by id."""
528
+ sandbox = self._client.get(sandbox_id)
529
+ self._client.delete(sandbox)
530
+
531
+
532
+ class _ModalProvider(SandboxProvider):
533
+ """Modal sandbox provider — lifecycle management for Modal sandboxes."""
534
+
535
+ def __init__(self) -> None:
536
+ self._modal = _import_provider_module(
537
+ "modal",
538
+ provider="modal",
539
+ package="langchain-modal",
540
+ )
541
+
542
+ from deepagents_code.model_config import resolve_env_var
543
+
544
+ token_id = resolve_env_var("MODAL_TOKEN_ID")
545
+ token_secret = resolve_env_var("MODAL_TOKEN_SECRET")
546
+ if token_id and token_secret:
547
+ try:
548
+ self._client = self._modal.Client.from_credentials(
549
+ token_id, token_secret
550
+ )
551
+ except Exception as exc:
552
+ msg = (
553
+ "Failed to authenticate with Modal using "
554
+ "MODAL_TOKEN_ID / MODAL_TOKEN_SECRET "
555
+ "(or the DEEPAGENTS_CODE_-prefixed equivalents). "
556
+ "Verify your credentials are valid."
557
+ )
558
+ raise ValueError(msg) from exc
559
+ elif token_id or token_secret:
560
+ logger.warning(
561
+ "Only one of MODAL_TOKEN_ID / MODAL_TOKEN_SECRET is set; "
562
+ "both are required for explicit credential auth. "
563
+ "Falling back to default Modal authentication.",
564
+ )
565
+ self._client = None
566
+ else:
567
+ self._client = None
568
+
569
+ lookup_kwargs: dict[str, Any] = {
570
+ "name": "deepagents-sandbox",
571
+ "create_if_missing": True,
572
+ }
573
+ if self._client is not None:
574
+ lookup_kwargs["client"] = self._client
575
+ self._app = self._modal.App.lookup(**lookup_kwargs)
576
+
577
+ def get_or_create(
578
+ self,
579
+ *,
580
+ sandbox_id: str | None = None,
581
+ timeout: int = 180,
582
+ **kwargs: Any, # noqa: ARG002
583
+ ) -> SandboxBackendProtocol:
584
+ """Get or create a Modal sandbox.
585
+
586
+ Args:
587
+ sandbox_id: Existing sandbox ID, or None to create.
588
+ timeout: Seconds to wait for startup.
589
+ **kwargs: Unused.
590
+
591
+ Returns:
592
+ `ModalSandbox` instance.
593
+
594
+ Raises:
595
+ RuntimeError: If the sandbox fails to start.
596
+ """
597
+ modal_backend = _import_provider_module(
598
+ "langchain_modal",
599
+ provider="modal",
600
+ package="langchain-modal",
601
+ )
602
+
603
+ client_kwargs: dict[str, Any] = {}
604
+ if self._client is not None:
605
+ client_kwargs["client"] = self._client
606
+
607
+ if sandbox_id:
608
+ sandbox = self._modal.Sandbox.from_id(
609
+ sandbox_id=sandbox_id,
610
+ app=self._app,
611
+ **client_kwargs,
612
+ )
613
+ else:
614
+ sandbox = self._modal.Sandbox.create(
615
+ app=self._app, workdir="/workspace", **client_kwargs
616
+ )
617
+ last_exc: Exception | None = None
618
+ for _ in range(timeout // 2):
619
+ if sandbox.poll() is not None:
620
+ msg = "Modal sandbox terminated unexpectedly during startup"
621
+ raise RuntimeError(msg)
622
+ try:
623
+ process = sandbox.exec("echo", "ready", timeout=5)
624
+ process.wait()
625
+ if process.returncode == 0:
626
+ break
627
+ except Exception as exc: # noqa: BLE001 # Transient failures expected during readiness polling
628
+ last_exc = exc
629
+ time.sleep(2)
630
+ else:
631
+ sandbox.terminate()
632
+ detail = f" Last error: {last_exc}" if last_exc else ""
633
+ msg = f"Modal sandbox failed to start within {timeout} seconds.{detail}"
634
+ raise RuntimeError(msg)
635
+
636
+ return modal_backend.ModalSandbox(sandbox=sandbox)
637
+
638
+ def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002
639
+ """Terminate a Modal sandbox by id."""
640
+ del_kwargs: dict[str, Any] = {"sandbox_id": sandbox_id, "app": self._app}
641
+ if self._client is not None:
642
+ del_kwargs["client"] = self._client
643
+ sandbox = self._modal.Sandbox.from_id(**del_kwargs)
644
+ sandbox.terminate()
645
+
646
+
647
+ class _RunloopProvider(SandboxProvider):
648
+ """Runloop sandbox provider — delegates to `langchain_runloop.RunloopProvider`."""
649
+
650
+ def __init__(self) -> None:
651
+ runloop_module = _import_provider_module(
652
+ "langchain_runloop",
653
+ provider="runloop",
654
+ package="langchain-runloop",
655
+ )
656
+
657
+ from deepagents_code.model_config import resolve_env_var
658
+
659
+ api_key = resolve_env_var("RUNLOOP_API_KEY")
660
+ if not api_key:
661
+ msg = (
662
+ "No Runloop API key found. Set RUNLOOP_API_KEY "
663
+ "or DEEPAGENTS_CODE_RUNLOOP_API_KEY."
664
+ )
665
+ raise ValueError(msg)
666
+ self._provider = runloop_module.RunloopProvider(
667
+ api_key=api_key,
668
+ resolve_env_var=resolve_env_var,
669
+ )
670
+
671
+ def get_or_create(
672
+ self,
673
+ *,
674
+ sandbox_id: str | None = None,
675
+ timeout: int = 180,
676
+ **kwargs: Any,
677
+ ) -> SandboxBackendProtocol:
678
+ """Get or create a Runloop devbox.
679
+
680
+ Args:
681
+ sandbox_id: Existing devbox ID, or None to create.
682
+ timeout: Accepted for parity with other providers; currently
683
+ forwarded but unused by the Runloop backend (the SDK manages
684
+ its own startup wait).
685
+ **kwargs: Runloop-specific options (`snapshot` blueprint name,
686
+ `blueprint_dockerfile`).
687
+
688
+ Returns:
689
+ `RunloopSandbox` instance.
690
+
691
+ Raises:
692
+ SandboxNotFoundError: If `sandbox_id` does not exist. `RunloopProvider`
693
+ translates the SDK's not-found error into a `KeyError`, which is
694
+ mapped here.
695
+ KeyError: If a `KeyError` is raised while no `sandbox_id` was supplied
696
+ (re-raised unchanged rather than mislabeled as not-found).
697
+ """
698
+ try:
699
+ return self._provider.get_or_create(
700
+ sandbox_id=sandbox_id,
701
+ timeout=timeout,
702
+ **kwargs,
703
+ )
704
+ except KeyError as e:
705
+ if sandbox_id is None:
706
+ raise
707
+ raise SandboxNotFoundError(sandbox_id) from e
708
+
709
+ def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002
710
+ """Shut down a Runloop devbox by id."""
711
+ self._provider.delete(sandbox_id=sandbox_id)
712
+
713
+
714
+ class _AgentCoreProvider(SandboxProvider):
715
+ """AgentCore Code Interpreter sandbox provider.
716
+
717
+ Manages AgentCore session lifecycle. Sessions cannot be reconnected after
718
+ the app exits — the `sandbox_id` parameter is not supported.
719
+ """
720
+
721
+ def __init__(self, region: str | None = None) -> None:
722
+ """Initialize AgentCore provider.
723
+
724
+ Args:
725
+ region: AWS region (defaults to `AWS_REGION` /
726
+ `AWS_DEFAULT_REGION` / `us-west-2`).
727
+
728
+ Raises:
729
+ ValueError: If boto3 is installed and AWS credentials cannot
730
+ be resolved.
731
+ """
732
+ self._region = region or os.environ.get(
733
+ "AWS_REGION", os.environ.get("AWS_DEFAULT_REGION", "us-west-2")
734
+ )
735
+
736
+ # Validate AWS credentials early for a clear error message.
737
+ try:
738
+ import boto3 # ty: ignore[unresolved-import]
739
+
740
+ session = boto3.Session()
741
+ credentials = session.get_credentials()
742
+ if credentials is None:
743
+ msg = (
744
+ "AWS credentials not found. Configure via "
745
+ "AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN, "
746
+ "~/.aws/credentials, or an IAM role."
747
+ )
748
+ raise ValueError(msg) # noqa: TRY301 # intentional raise for early credential validation
749
+ except ImportError:
750
+ logger.debug("boto3 not installed; skipping credential pre-check")
751
+ except ValueError:
752
+ raise
753
+ except Exception:
754
+ logger.warning(
755
+ "AWS credential pre-validation failed — the session may "
756
+ "fail to start. Check your AWS configuration.",
757
+ exc_info=True,
758
+ )
759
+
760
+ self._active_interpreters: dict[str, Any] = {}
761
+
762
+ def get_or_create(
763
+ self,
764
+ *,
765
+ sandbox_id: str | None = None,
766
+ **kwargs: Any, # noqa: ARG002 # required by SandboxProvider interface
767
+ ) -> SandboxBackendProtocol:
768
+ """Create a new AgentCore Code Interpreter session.
769
+
770
+ Args:
771
+ sandbox_id: Not supported — raises `NotImplementedError`
772
+ if provided.
773
+ **kwargs: Additional parameters (unused).
774
+
775
+ Returns:
776
+ `AgentCoreSandbox` instance wrapping the started interpreter.
777
+
778
+ Raises:
779
+ NotImplementedError: If `sandbox_id` is provided.
780
+ """
781
+ if sandbox_id:
782
+ msg = (
783
+ "AgentCore does not support reconnecting to existing sessions. "
784
+ "Remove the --sandbox-id option."
785
+ )
786
+ raise NotImplementedError(msg)
787
+
788
+ agentcore_module = _import_provider_module(
789
+ "bedrock_agentcore.tools.code_interpreter_client",
790
+ provider="agentcore",
791
+ package="langchain-agentcore-codeinterpreter",
792
+ )
793
+ agentcore_backend = _import_provider_module(
794
+ "langchain_agentcore_codeinterpreter",
795
+ provider="agentcore",
796
+ package="langchain-agentcore-codeinterpreter",
797
+ )
798
+
799
+ interpreter = agentcore_module.CodeInterpreter(
800
+ region=self._region,
801
+ integration_source="deepagents-code",
802
+ )
803
+ try:
804
+ interpreter.start()
805
+ except Exception:
806
+ with contextlib.suppress(Exception):
807
+ interpreter.stop()
808
+ raise
809
+
810
+ backend = agentcore_backend.AgentCoreSandbox(interpreter=interpreter)
811
+ self._active_interpreters[backend.id] = interpreter
812
+ return backend
813
+
814
+ def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002 # required by SandboxProvider interface
815
+ """Stop an AgentCore session.
816
+
817
+ Args:
818
+ sandbox_id: Session ID to stop.
819
+ **kwargs: Additional parameters (unused).
820
+ """
821
+ interpreter = self._active_interpreters.pop(sandbox_id, None)
822
+ if interpreter:
823
+ try:
824
+ interpreter.stop()
825
+ logger.info("AgentCore session %s stopped", sandbox_id)
826
+ except Exception:
827
+ logger.warning(
828
+ "Failed to stop AgentCore session %s — the session may "
829
+ "still be running and incurring costs. Check the AWS "
830
+ "console to verify.",
831
+ sandbox_id,
832
+ exc_info=True,
833
+ )
834
+ else:
835
+ logger.info(
836
+ "AgentCore session %s not tracked (may have already expired)",
837
+ sandbox_id,
838
+ )
839
+
840
+
841
+ _VercelStatus = Literal[
842
+ "pending",
843
+ "running",
844
+ "stopping",
845
+ "stopped",
846
+ "failed",
847
+ "aborted",
848
+ "snapshotting",
849
+ ]
850
+ """Vercel sandbox lifecycle statuses (mirrors the SDK's `SandboxStatus` enum)."""
851
+
852
+ _VERCEL_TERMINAL_STATUSES: frozenset[_VercelStatus] = frozenset(
853
+ {"aborted", "failed", "stopped"}
854
+ )
855
+ """Vercel sandbox statuses that cannot become ready without a new sandbox.
856
+
857
+ Typing the members as `_VercelStatus` turns a typo into a type error rather than
858
+ a silently-never-matching string.
859
+ """
860
+
861
+ _VERCEL_DEFAULT_RUNTIME = "python3.13"
862
+ """Runtime used when creating a fresh Vercel sandbox."""
863
+
864
+ _VERCEL_SANDBOX_TIMEOUT = timedelta(minutes=30)
865
+ """Lifetime used for freshly created Vercel sandboxes."""
866
+
867
+
868
+ class _VercelSandboxHandle(Protocol):
869
+ """Minimal Vercel SDK sandbox surface used by the built-in provider."""
870
+
871
+ sandbox_id: str
872
+ status: str
873
+
874
+ def wait_for_status(self, status: str, *, timeout: int) -> None:
875
+ """Wait for the sandbox to reach the requested status."""
876
+
877
+ def stop(self) -> None:
878
+ """Stop the sandbox."""
879
+
880
+
881
+ class _VercelProvider(SandboxProvider):
882
+ """Vercel Sandbox provider implementation."""
883
+
884
+ _CREDENTIAL_ENV_NAMES = (
885
+ "VERCEL_TOKEN",
886
+ "VERCEL_PROJECT_ID",
887
+ "VERCEL_TEAM_ID",
888
+ )
889
+
890
+ def __init__(self) -> None:
891
+ self._sdk_kwargs = self._resolve_sdk_kwargs()
892
+
893
+ @classmethod
894
+ def _resolve_sdk_kwargs(cls) -> dict[str, str]:
895
+ """Resolve explicit Vercel credentials when a prefixed override is set.
896
+
897
+ Credentials stay SDK-managed (canonical `VERCEL_*` variables or OIDC)
898
+ unless at least one of the prefixed overrides
899
+ `DEEPAGENTS_CODE_VERCEL_TOKEN`, `DEEPAGENTS_CODE_VERCEL_PROJECT_ID`, or
900
+ `DEEPAGENTS_CODE_VERCEL_TEAM_ID` is present. When triggered, each value
901
+ still resolves prefixed-first then canonical via `resolve_env_var`.
902
+
903
+ Returns:
904
+ Explicit SDK credential arguments, or an empty mapping to delegate
905
+ credential resolution to the Vercel SDK.
906
+ """
907
+ prefix = "DEEPAGENTS_CODE_"
908
+ has_override = any(
909
+ f"{prefix}{name}" in os.environ for name in cls._CREDENTIAL_ENV_NAMES
910
+ )
911
+ if not has_override:
912
+ return {}
913
+
914
+ from deepagents_code.model_config import resolve_env_var
915
+
916
+ values = {
917
+ "token": resolve_env_var("VERCEL_TOKEN"),
918
+ "project_id": resolve_env_var("VERCEL_PROJECT_ID"),
919
+ "team_id": resolve_env_var("VERCEL_TEAM_ID"),
920
+ }
921
+ missing = sorted(key for key, value in values.items() if not value)
922
+ if missing:
923
+ logger.warning(
924
+ "Incomplete explicit Vercel credentials; VERCEL_TOKEN, "
925
+ "VERCEL_PROJECT_ID, and VERCEL_TEAM_ID are all required. "
926
+ "Falling back to default Vercel authentication. Missing: %s",
927
+ ", ".join(missing),
928
+ )
929
+ return {}
930
+ return {key: value for key, value in values.items() if value}
931
+
932
+ def get_or_create(
933
+ self,
934
+ *,
935
+ sandbox_id: str | None = None,
936
+ timeout: int = 180,
937
+ **kwargs: Any,
938
+ ) -> SandboxBackendProtocol:
939
+ """Get or create a Vercel sandbox.
940
+
941
+ Args:
942
+ sandbox_id: Existing sandbox ID, or None to create.
943
+ timeout: Seconds to wait for startup.
944
+ **kwargs: Rejected; passing any keyword argument raises `TypeError`.
945
+
946
+ Returns:
947
+ `VercelSandbox` instance.
948
+
949
+ Raises:
950
+ RuntimeError: If the sandbox fails to start.
951
+ TypeError: If unsupported keyword arguments are provided.
952
+ """
953
+ if kwargs:
954
+ msg = f"Received unsupported arguments: {list(kwargs.keys())}"
955
+ raise TypeError(msg)
956
+
957
+ vercel_backend = _import_provider_module(
958
+ "langchain_vercel_sandbox",
959
+ provider="vercel",
960
+ package="langchain-vercel-sandbox",
961
+ )
962
+ vercel_sandbox = _import_provider_module(
963
+ "vercel.sandbox",
964
+ provider="vercel",
965
+ package="vercel",
966
+ )
967
+
968
+ # The SDK is imported dynamically, so `Sandbox.get`/`create` are typed
969
+ # `Any`. Annotating pins the result to the Protocol so the type checker
970
+ # verifies the `.status`/`.wait_for_status`/`.stop` calls below.
971
+ sandbox: _VercelSandboxHandle
972
+ try:
973
+ if sandbox_id:
974
+ sandbox = vercel_sandbox.Sandbox.get(
975
+ sandbox_id=sandbox_id,
976
+ **self._sdk_kwargs,
977
+ )
978
+ else:
979
+ sandbox = vercel_sandbox.Sandbox.create(
980
+ runtime=_VERCEL_DEFAULT_RUNTIME,
981
+ timeout=_VERCEL_SANDBOX_TIMEOUT,
982
+ **self._sdk_kwargs,
983
+ )
984
+ except Exception as exc: # Vercel SDK exception types vary by version
985
+ # Keep the message generic so SDK errors cannot leak credentials,
986
+ # but chain the original exception so tracebacks retain root cause.
987
+ action = "connect to existing" if sandbox_id else "create"
988
+ msg = f"Failed to {action} Vercel sandbox."
989
+ raise RuntimeError(msg) from exc
990
+
991
+ try:
992
+ self._wait_until_running(sandbox, timeout=timeout)
993
+ except Exception:
994
+ if sandbox_id is None:
995
+ with contextlib.suppress(Exception):
996
+ sandbox.stop()
997
+ raise
998
+
999
+ return vercel_backend.VercelSandbox(sandbox=sandbox)
1000
+
1001
+ def delete(self, *, sandbox_id: str, **kwargs: Any) -> None: # noqa: ARG002 # **kwargs required by SandboxProvider interface
1002
+ """Stop a Vercel sandbox by id.
1003
+
1004
+ Raises:
1005
+ RuntimeError: If the Vercel SDK cannot find or stop the sandbox.
1006
+ """
1007
+ vercel_sandbox = _import_provider_module(
1008
+ "vercel.sandbox",
1009
+ provider="vercel",
1010
+ package="vercel",
1011
+ )
1012
+ try:
1013
+ sandbox = vercel_sandbox.Sandbox.get(
1014
+ sandbox_id=sandbox_id,
1015
+ **self._sdk_kwargs,
1016
+ )
1017
+ sandbox.stop()
1018
+ except Exception as exc: # Vercel SDK exception types vary by version
1019
+ # Generic message avoids leaking credentials; chain preserves cause.
1020
+ msg = "Failed to stop Vercel sandbox."
1021
+ raise RuntimeError(msg) from exc
1022
+
1023
+ @staticmethod
1024
+ def _wait_until_running(
1025
+ sandbox: _VercelSandboxHandle,
1026
+ *,
1027
+ timeout: int,
1028
+ ) -> None:
1029
+ """Wait for a Vercel sandbox to become ready.
1030
+
1031
+ Raises:
1032
+ RuntimeError: If the sandbox reaches a terminal state or does not
1033
+ become ready before the timeout.
1034
+ """
1035
+ status = str(sandbox.status)
1036
+ if status == "running":
1037
+ return
1038
+ if status in _VERCEL_TERMINAL_STATUSES:
1039
+ msg = f"Vercel sandbox {sandbox.sandbox_id} is in terminal state {status!r}"
1040
+ raise RuntimeError(msg)
1041
+
1042
+ try:
1043
+ sandbox.wait_for_status("running", timeout=timeout)
1044
+ except TimeoutError as exc:
1045
+ status = str(sandbox.status)
1046
+ msg = (
1047
+ f"Vercel sandbox {sandbox.sandbox_id} failed to start within "
1048
+ f"{timeout} seconds; current status is {status!r}"
1049
+ )
1050
+ raise RuntimeError(msg) from exc
1051
+ except Exception as exc: # Vercel SDK exception types vary by version
1052
+ # Generic message avoids leaking credentials; chain preserves cause.
1053
+ msg = "Failed while waiting for Vercel sandbox startup."
1054
+ raise RuntimeError(msg) from exc
1055
+
1056
+
1057
+ def _get_provider(
1058
+ provider_name: str,
1059
+ registry: SandboxRegistry | None = None,
1060
+ ) -> SandboxProvider:
1061
+ """Get a `SandboxProvider` instance for the specified provider (internal).
1062
+
1063
+ Args:
1064
+ provider_name: Name of the provider. Resolved through the registry so
1065
+ built-in, entry-point, and config providers are all supported.
1066
+ registry: An already-built registry to reuse. A fresh one is loaded
1067
+ when omitted.
1068
+
1069
+ Returns:
1070
+ `SandboxProvider` instance. Propagates `ValueError` from the registry
1071
+ if `provider_name` is unknown.
1072
+ """
1073
+ reg = registry if registry is not None else _get_registry()
1074
+ return reg.create_provider(provider_name)
1075
+
1076
+
1077
+ def verify_sandbox_deps(provider: str) -> None:
1078
+ """Check that the required packages for a sandbox provider are installed.
1079
+
1080
+ Uses `importlib.util.find_spec` for a lightweight check with no actual
1081
+ imports. Call this in the app's process *before* spawning the server
1082
+ subprocess so users get a clear, actionable error instead of an opaque
1083
+ server crash. The backend module to probe and the install hint both come
1084
+ from provider metadata.
1085
+
1086
+ Args:
1087
+ provider: Sandbox provider name (e.g. `'daytona'`).
1088
+
1089
+ Raises:
1090
+ ImportError: If the provider's backend package is not installed.
1091
+ """
1092
+ if not provider or provider == "none":
1093
+ return
1094
+
1095
+ metadata = _get_registry().get_metadata(provider)
1096
+ if metadata is None or metadata.backend_module is None:
1097
+ logger.debug(
1098
+ "No backend module to probe for provider %r; skipping pre-flight check",
1099
+ provider,
1100
+ )
1101
+ return
1102
+
1103
+ try:
1104
+ found = importlib.util.find_spec(metadata.backend_module) is not None
1105
+ except (ImportError, ValueError):
1106
+ found = False
1107
+
1108
+ if not found:
1109
+ if metadata.install is not None:
1110
+ install_hint = (
1111
+ f"Install with: {metadata.install.command(in_app=True)} (in-app) "
1112
+ f"or {metadata.install.command(in_app=False)} (CLI)"
1113
+ )
1114
+ else:
1115
+ install_hint = "Install the provider's package."
1116
+ msg = f"Missing dependencies for '{provider}' sandbox. {install_hint}"
1117
+ raise ImportError(msg)
1118
+
1119
+
1120
+ __all__ = [
1121
+ "create_sandbox",
1122
+ "get_default_working_dir",
1123
+ "verify_sandbox_deps",
1124
+ ]