2026-04-06 10:24:50 -07:00
|
|
|
"""LangSmith sandbox backend integration."""
|
2026-02-10 13:35:50 -08:00
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
fix: enforce client-side deadline on sandbox execute (#1451)
* fix: enforce client-side deadline on sandbox execute
The langsmith SDK's default execute path is now a WebSocket stream with
no client-side read deadline. On a live socket where the dataplane never
emits an exit/error frame, CommandHandle.result blocks forever in an
uncancellable thread, wedging the run (introduced by the langsmith
0.8.3 -> 0.8.8 bump in #1385). The command `timeout` is only enforced
server-side, so it doesn't fire.
TimeoutLangSmithSandbox drives a non-blocking CommandHandle and kills the
command if it overruns its timeout by a grace window
(SANDBOX_EXECUTE_CLIENT_GRACE_SECONDS, default 30), returning a timed-out
tool result instead of hanging. WS connect failures fall back to the base
wait=True path, whose HTTP fallback carries its own request deadline.
* fix: fall back to HTTP when WS execute connect fails
run(wait=False) eagerly opens the WebSocket and reads the "started" frame,
so connect/setup failures (and connect timeouts) raise from the run() call
itself, not from handle.result. The previous structure left run() outside
the try, so those failures bypassed the HTTP fallback and would fail every
sandbox command in any environment where the WS path is unavailable.
Move handle creation inside the fallback handler in both execute and
aexecute, and run it via to_thread in the async path since it now blocks on
connect. Add tests for connect-failure and connect-timeout fallback.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-08 11:16:08 -07:00
|
|
|
import asyncio
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
import base64
|
|
|
|
|
import logging
|
2026-02-10 13:35:50 -08:00
|
|
|
import os
|
2026-06-04 13:51:57 -07:00
|
|
|
import time
|
2026-02-10 13:35:50 -08:00
|
|
|
from abc import ABC, abstractmethod
|
fix: enforce client-side deadline on sandbox execute (#1451)
* fix: enforce client-side deadline on sandbox execute
The langsmith SDK's default execute path is now a WebSocket stream with
no client-side read deadline. On a live socket where the dataplane never
emits an exit/error frame, CommandHandle.result blocks forever in an
uncancellable thread, wedging the run (introduced by the langsmith
0.8.3 -> 0.8.8 bump in #1385). The command `timeout` is only enforced
server-side, so it doesn't fire.
TimeoutLangSmithSandbox drives a non-blocking CommandHandle and kills the
command if it overruns its timeout by a grace window
(SANDBOX_EXECUTE_CLIENT_GRACE_SECONDS, default 30), returning a timed-out
tool result instead of hanging. WS connect failures fall back to the base
wait=True path, whose HTTP fallback carries its own request deadline.
* fix: fall back to HTTP when WS execute connect fails
run(wait=False) eagerly opens the WebSocket and reads the "started" frame,
so connect/setup failures (and connect timeouts) raise from the run() call
itself, not from handle.result. The previous structure left run() outside
the try, so those failures bypassed the HTTP fallback and would fail every
sandbox command in any environment where the WS path is unavailable.
Move handle creation inside the fallback handler in both execute and
aexecute, and run it via to_thread in the async path since it now blocks on
connect. Add tests for connect-failure and connect-timeout fallback.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-08 11:16:08 -07:00
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
|
from concurrent.futures import TimeoutError as FuturesTimeout
|
2026-02-20 09:58:07 -08:00
|
|
|
from typing import Any
|
2026-02-10 13:35:50 -08:00
|
|
|
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
import httpx
|
2026-04-06 10:24:50 -07:00
|
|
|
from deepagents.backends import LangSmithSandbox
|
fix: enforce client-side deadline on sandbox execute (#1451)
* fix: enforce client-side deadline on sandbox execute
The langsmith SDK's default execute path is now a WebSocket stream with
no client-side read deadline. On a live socket where the dataplane never
emits an exit/error frame, CommandHandle.result blocks forever in an
uncancellable thread, wedging the run (introduced by the langsmith
0.8.3 -> 0.8.8 bump in #1385). The command `timeout` is only enforced
server-side, so it doesn't fire.
TimeoutLangSmithSandbox drives a non-blocking CommandHandle and kills the
command if it overruns its timeout by a grace window
(SANDBOX_EXECUTE_CLIENT_GRACE_SECONDS, default 30), returning a timed-out
tool result instead of hanging. WS connect failures fall back to the base
wait=True path, whose HTTP fallback carries its own request deadline.
* fix: fall back to HTTP when WS execute connect fails
run(wait=False) eagerly opens the WebSocket and reads the "started" frame,
so connect/setup failures (and connect timeouts) raise from the run() call
itself, not from handle.result. The previous structure left run() outside
the try, so those failures bypassed the HTTP fallback and would fail every
sandbox command in any environment where the WS path is unavailable.
Move handle creation inside the fallback handler in both execute and
aexecute, and run it via to_thread in the async path since it now blocks on
connect. Add tests for connect-failure and connect-timeout fallback.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-08 11:16:08 -07:00
|
|
|
from deepagents.backends.protocol import ExecuteResponse, SandboxBackendProtocol
|
|
|
|
|
from langsmith.sandbox import (
|
|
|
|
|
CommandTimeoutError,
|
|
|
|
|
SandboxClient,
|
|
|
|
|
SandboxConnectionError,
|
|
|
|
|
SandboxServerReloadError,
|
|
|
|
|
)
|
2026-02-10 13:35:50 -08:00
|
|
|
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2026-04-21 15:17:19 -04:00
|
|
|
DEFAULT_SNAPSHOT_FS_CAPACITY_BYTES = 32 * 1024**3
|
2026-04-30 13:24:15 -07:00
|
|
|
DEFAULT_SANDBOX_VCPUS = 2
|
|
|
|
|
DEFAULT_SANDBOX_MEM_BYTES = 7936 * 1024**2 # 7936 MiB ("large" tier cap)
|
2026-06-19 22:05:58 -04:00
|
|
|
DEFAULT_SANDBOX_IDLE_TTL_SECONDS = 2 * 60 * 60 # 2 hours
|
2026-05-08 00:30:14 -04:00
|
|
|
DEFAULT_SANDBOX_DELETE_AFTER_STOP_SECONDS = 24 * 60 * 60 # 24 hours
|
2026-06-04 13:51:57 -07:00
|
|
|
PROXY_CONFIG_MAX_ATTEMPTS = 3
|
|
|
|
|
PROXY_CONFIG_TIMEOUT_SECONDS = 10.0
|
|
|
|
|
PROXY_CONFIG_RETRY_DELAYS_SECONDS = (0.5, 1.0)
|
|
|
|
|
PROXY_CONFIG_RETRYABLE_STATUS_CODES = frozenset({408, 409, 425, 429, 500, 502, 503, 504, 529})
|
2026-04-21 15:17:19 -04:00
|
|
|
|
2026-02-10 13:35:50 -08:00
|
|
|
|
2026-02-12 17:39:59 -08:00
|
|
|
def _get_langsmith_api_key() -> str | None:
|
|
|
|
|
"""Get LangSmith API key from environment.
|
|
|
|
|
|
|
|
|
|
Checks LANGSMITH_API_KEY first, then falls back to LANGSMITH_API_KEY_PROD
|
|
|
|
|
for LangGraph Cloud deployments where LANGSMITH_API_KEY is reserved.
|
|
|
|
|
"""
|
|
|
|
|
return os.environ.get("LANGSMITH_API_KEY") or os.environ.get("LANGSMITH_API_KEY_PROD")
|
|
|
|
|
|
|
|
|
|
|
2026-05-08 00:30:14 -04:00
|
|
|
def _parse_optional_int(name: str, default: int) -> int:
|
|
|
|
|
raw = os.environ.get(name)
|
|
|
|
|
if not raw:
|
|
|
|
|
return default
|
|
|
|
|
try:
|
|
|
|
|
return int(raw)
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
msg = f"{name} must be an integer, got {raw!r}"
|
|
|
|
|
raise ValueError(msg) from e
|
|
|
|
|
|
|
|
|
|
|
fix: enforce client-side deadline on sandbox execute (#1451)
* fix: enforce client-side deadline on sandbox execute
The langsmith SDK's default execute path is now a WebSocket stream with
no client-side read deadline. On a live socket where the dataplane never
emits an exit/error frame, CommandHandle.result blocks forever in an
uncancellable thread, wedging the run (introduced by the langsmith
0.8.3 -> 0.8.8 bump in #1385). The command `timeout` is only enforced
server-side, so it doesn't fire.
TimeoutLangSmithSandbox drives a non-blocking CommandHandle and kills the
command if it overruns its timeout by a grace window
(SANDBOX_EXECUTE_CLIENT_GRACE_SECONDS, default 30), returning a timed-out
tool result instead of hanging. WS connect failures fall back to the base
wait=True path, whose HTTP fallback carries its own request deadline.
* fix: fall back to HTTP when WS execute connect fails
run(wait=False) eagerly opens the WebSocket and reads the "started" frame,
so connect/setup failures (and connect timeouts) raise from the run() call
itself, not from handle.result. The previous structure left run() outside
the try, so those failures bypassed the HTTP fallback and would fail every
sandbox command in any environment where the WS path is unavailable.
Move handle creation inside the fallback handler in both execute and
aexecute, and run it via to_thread in the async path since it now blocks on
connect. Add tests for connect-failure and connect-timeout fallback.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-08 11:16:08 -07:00
|
|
|
def _execute_client_grace_seconds() -> int:
|
|
|
|
|
"""Extra wall-clock seconds the client waits past a command's own timeout
|
|
|
|
|
before giving up and killing it. The server is meant to enforce the command
|
|
|
|
|
timeout; this is the client-side backstop for when it doesn't."""
|
|
|
|
|
return _parse_optional_int("SANDBOX_EXECUTE_CLIENT_GRACE_SECONDS", 30)
|
|
|
|
|
|
|
|
|
|
|
2026-05-08 00:30:14 -04:00
|
|
|
def _get_sandbox_snapshot_config() -> tuple[str | None, int, int, int, int, int]:
|
2026-04-21 15:17:19 -04:00
|
|
|
"""Get sandbox snapshot configuration from environment."""
|
|
|
|
|
snapshot_id = os.environ.get("DEFAULT_SANDBOX_SNAPSHOT_ID")
|
2026-05-08 00:30:14 -04:00
|
|
|
fs_capacity_bytes = _parse_optional_int(
|
|
|
|
|
"DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES", DEFAULT_SNAPSHOT_FS_CAPACITY_BYTES
|
|
|
|
|
)
|
|
|
|
|
vcpus = _parse_optional_int("DEFAULT_SANDBOX_VCPUS", DEFAULT_SANDBOX_VCPUS)
|
|
|
|
|
mem_bytes = _parse_optional_int("DEFAULT_SANDBOX_MEM_BYTES", DEFAULT_SANDBOX_MEM_BYTES)
|
|
|
|
|
idle_ttl_seconds = _parse_optional_int(
|
|
|
|
|
"DEFAULT_SANDBOX_IDLE_TTL_SECONDS", DEFAULT_SANDBOX_IDLE_TTL_SECONDS
|
|
|
|
|
)
|
|
|
|
|
delete_after_stop_seconds = _parse_optional_int(
|
|
|
|
|
"DEFAULT_SANDBOX_DELETE_AFTER_STOP_SECONDS",
|
|
|
|
|
DEFAULT_SANDBOX_DELETE_AFTER_STOP_SECONDS,
|
|
|
|
|
)
|
|
|
|
|
return (
|
|
|
|
|
snapshot_id,
|
|
|
|
|
fs_capacity_bytes,
|
|
|
|
|
vcpus,
|
|
|
|
|
mem_bytes,
|
|
|
|
|
idle_ttl_seconds,
|
|
|
|
|
delete_after_stop_seconds,
|
|
|
|
|
)
|
2026-02-12 17:39:59 -08:00
|
|
|
|
|
|
|
|
|
2026-05-04 18:03:53 -07:00
|
|
|
def _github_proxy_rules(github_token: str) -> list[dict[str, Any]]:
|
|
|
|
|
basic_auth = base64.b64encode(f"x-access-token:{github_token}".encode()).decode()
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
"name": "github-api",
|
|
|
|
|
"match_hosts": ["api.github.com"],
|
|
|
|
|
"headers": [
|
|
|
|
|
{
|
|
|
|
|
"name": "Authorization",
|
|
|
|
|
"type": "opaque",
|
|
|
|
|
"value": f"Bearer {github_token}",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"name": "github",
|
|
|
|
|
"match_hosts": ["github.com", "*.github.com"],
|
|
|
|
|
"headers": [
|
|
|
|
|
{
|
|
|
|
|
"name": "Authorization",
|
|
|
|
|
"type": "opaque",
|
|
|
|
|
"value": f"Basic {basic_auth}",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
2026-06-04 13:51:57 -07:00
|
|
|
def _retry_after_seconds(response: httpx.Response | None) -> float | None:
|
|
|
|
|
if response is None:
|
|
|
|
|
return None
|
|
|
|
|
raw = response.headers.get("Retry-After")
|
|
|
|
|
if not raw:
|
|
|
|
|
return None
|
|
|
|
|
try:
|
|
|
|
|
delay = float(raw)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return None
|
|
|
|
|
return max(delay, 0.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_retryable_proxy_config_error(exc: BaseException) -> bool:
|
|
|
|
|
if isinstance(exc, httpx.HTTPStatusError):
|
|
|
|
|
return exc.response.status_code in PROXY_CONFIG_RETRYABLE_STATUS_CODES
|
|
|
|
|
return isinstance(exc, httpx.TransportError)
|
|
|
|
|
|
|
|
|
|
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
def _configure_github_proxy(sandbox_name: str, github_token: str) -> None:
|
2026-05-04 18:03:53 -07:00
|
|
|
"""Configure sandbox proxy to inject GitHub auth for GitHub traffic.
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
|
|
|
|
|
Uses the LangSmith proxy-config API to set up header injection so that
|
|
|
|
|
git operations (clone, pull, push) authenticate via the proxy rather than
|
|
|
|
|
writing credentials to disk in the sandbox.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
sandbox_name: The sandbox name/ID returned by the LangSmith API.
|
|
|
|
|
github_token: GitHub token to inject as Authorization header.
|
|
|
|
|
"""
|
|
|
|
|
api_key = _get_langsmith_api_key()
|
|
|
|
|
if not api_key:
|
|
|
|
|
logger.warning("No LangSmith API key found, skipping GitHub proxy configuration")
|
|
|
|
|
return
|
|
|
|
|
langsmith_endpoint = os.environ.get("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com")
|
|
|
|
|
url = f"{langsmith_endpoint}/v2/sandboxes/boxes/{sandbox_name}"
|
2026-05-04 18:03:53 -07:00
|
|
|
payload = {"proxy_config": {"rules": _github_proxy_rules(github_token)}}
|
2026-06-04 13:51:57 -07:00
|
|
|
with httpx.Client(timeout=PROXY_CONFIG_TIMEOUT_SECONDS) as client:
|
|
|
|
|
for attempt in range(PROXY_CONFIG_MAX_ATTEMPTS):
|
|
|
|
|
try:
|
|
|
|
|
response = client.patch(
|
|
|
|
|
url,
|
|
|
|
|
json=payload,
|
|
|
|
|
headers={"X-API-Key": api_key},
|
|
|
|
|
)
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
break
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
if attempt == PROXY_CONFIG_MAX_ATTEMPTS - 1 or not _is_retryable_proxy_config_error(
|
|
|
|
|
exc
|
|
|
|
|
):
|
|
|
|
|
raise
|
|
|
|
|
retry_after = (
|
|
|
|
|
_retry_after_seconds(exc.response)
|
|
|
|
|
if isinstance(exc, httpx.HTTPStatusError)
|
|
|
|
|
else None
|
|
|
|
|
)
|
|
|
|
|
delay = (
|
|
|
|
|
retry_after
|
|
|
|
|
or PROXY_CONFIG_RETRY_DELAYS_SECONDS[
|
|
|
|
|
min(attempt, len(PROXY_CONFIG_RETRY_DELAYS_SECONDS) - 1)
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
logger.warning(
|
|
|
|
|
"Failed to configure GitHub proxy for sandbox %s (%s); retrying in %.1fs",
|
|
|
|
|
sandbox_name,
|
|
|
|
|
type(exc).__name__,
|
|
|
|
|
delay,
|
|
|
|
|
)
|
|
|
|
|
time.sleep(delay)
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
logger.info("Configured GitHub proxy for sandbox %s", sandbox_name)
|
|
|
|
|
|
|
|
|
|
|
2026-02-24 08:14:31 -08:00
|
|
|
def create_langsmith_sandbox(
|
2026-02-12 17:39:59 -08:00
|
|
|
sandbox_id: str | None = None,
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
github_token: str | None = None,
|
2026-06-23 12:24:11 -07:00
|
|
|
*,
|
|
|
|
|
snapshot_id: str | None = None,
|
2026-02-12 17:39:59 -08:00
|
|
|
) -> SandboxBackendProtocol:
|
|
|
|
|
"""Create or connect to a LangSmith sandbox without automatic cleanup.
|
|
|
|
|
|
feat: stop auto-cloning and let agent manage repo setup [closes OPE-21] (#1159)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* feat: stop auto-cloning and let agent manage repo setup [closes OPE-21]
* feat: stop auto-cloning and let agent manage repo setup [closes OPE-21]
* fix: address review feedback — restore agents_md, add git user config, lint fixes
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* feat: add installation token auth to list_repos GitHub API call
* agents.md update
* linting
* fix: address PR review feedback — shell precedence bug in prompt, remove dead code
* linting
* Apply suggestion from @bracesproul
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
* Apply suggestion from @bracesproul
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
* fix: address PR review feedback — restore {working_dir} in prompt, remove clone code block
* fix:Extract check_or_recreate_sandbox utility from inline sandbox health check
* fix: address PR review feedback — async list_repos, restore template name, fix prompt colon
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
* linting
* yogesh/ope-21-stop-auto-cloning
* Update agent/tools/list_repos.py
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
* Update agent/prompt.py
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
* feat: address PR review — list_repos uses GitHub API only, PR trigger includes org/repo
* linting
* feat: address PR review feedback — list_repos pagination, simpler return, sandbox health check
* feat: support listing repos for personal user accounts via is_organization flag
---------
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
2026-04-10 17:04:55 -07:00
|
|
|
This function directly uses the LangSmithProvider to create/connect to sandboxes
|
|
|
|
|
without the context manager cleanup, allowing sandboxes to persist across
|
|
|
|
|
multiple agent invocations.
|
|
|
|
|
|
2026-02-12 17:39:59 -08:00
|
|
|
Args:
|
|
|
|
|
sandbox_id: Optional existing sandbox ID to connect to.
|
|
|
|
|
If None, creates a new sandbox.
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
github_token: Optional GitHub token. Used to configure proxy auth on
|
|
|
|
|
new sandboxes. Ignored when connecting to an existing sandbox.
|
2026-06-23 12:24:11 -07:00
|
|
|
snapshot_id: Optional repo-scoped snapshot to boot from. When omitted,
|
|
|
|
|
falls back to DEFAULT_SANDBOX_SNAPSHOT_ID.
|
2026-02-12 17:39:59 -08:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
SandboxBackendProtocol instance
|
|
|
|
|
"""
|
|
|
|
|
api_key = _get_langsmith_api_key()
|
2026-05-08 00:30:14 -04:00
|
|
|
(
|
2026-06-23 12:24:11 -07:00
|
|
|
default_snapshot_id,
|
2026-05-08 00:30:14 -04:00
|
|
|
fs_capacity_bytes,
|
|
|
|
|
vcpus,
|
|
|
|
|
mem_bytes,
|
|
|
|
|
idle_ttl_seconds,
|
|
|
|
|
delete_after_stop_seconds,
|
|
|
|
|
) = _get_sandbox_snapshot_config()
|
2026-02-12 17:39:59 -08:00
|
|
|
|
2026-06-23 12:24:11 -07:00
|
|
|
effective_snapshot_id = snapshot_id or default_snapshot_id
|
|
|
|
|
|
2026-02-12 17:39:59 -08:00
|
|
|
provider = LangSmithProvider(api_key=api_key)
|
2026-02-24 16:41:33 -08:00
|
|
|
backend = provider.get_or_create(
|
2026-02-12 17:39:59 -08:00
|
|
|
sandbox_id=sandbox_id,
|
2026-06-23 12:24:11 -07:00
|
|
|
snapshot_id=effective_snapshot_id,
|
2026-04-21 15:17:19 -04:00
|
|
|
fs_capacity_bytes=fs_capacity_bytes,
|
|
|
|
|
vcpus=vcpus,
|
|
|
|
|
mem_bytes=mem_bytes,
|
2026-05-08 00:30:14 -04:00
|
|
|
idle_ttl_seconds=idle_ttl_seconds,
|
|
|
|
|
delete_after_stop_seconds=delete_after_stop_seconds,
|
2026-02-12 17:39:59 -08:00
|
|
|
)
|
2026-02-24 16:41:33 -08:00
|
|
|
_update_thread_sandbox_metadata(backend.id)
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
|
|
|
|
|
if sandbox_id is None and github_token:
|
|
|
|
|
_configure_github_proxy(backend.id, github_token)
|
|
|
|
|
|
2026-02-24 16:41:33 -08:00
|
|
|
return backend
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _update_thread_sandbox_metadata(sandbox_id: str) -> None:
|
|
|
|
|
"""Update thread metadata with sandbox_id."""
|
|
|
|
|
try:
|
|
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
from langgraph.config import get_config
|
|
|
|
|
from langgraph_sdk import get_client
|
|
|
|
|
|
|
|
|
|
config = get_config()
|
|
|
|
|
thread_id = config.get("configurable", {}).get("thread_id")
|
|
|
|
|
if not thread_id:
|
|
|
|
|
return
|
|
|
|
|
client = get_client()
|
|
|
|
|
|
|
|
|
|
async def _update() -> None:
|
|
|
|
|
await client.threads.update(
|
|
|
|
|
thread_id=thread_id,
|
|
|
|
|
metadata={"sandbox_id": sandbox_id},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
loop = asyncio.get_running_loop()
|
|
|
|
|
except RuntimeError:
|
|
|
|
|
asyncio.run(_update())
|
|
|
|
|
else:
|
|
|
|
|
loop.create_task(_update())
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
2026-02-12 17:39:59 -08:00
|
|
|
|
|
|
|
|
|
fix: enforce client-side deadline on sandbox execute (#1451)
* fix: enforce client-side deadline on sandbox execute
The langsmith SDK's default execute path is now a WebSocket stream with
no client-side read deadline. On a live socket where the dataplane never
emits an exit/error frame, CommandHandle.result blocks forever in an
uncancellable thread, wedging the run (introduced by the langsmith
0.8.3 -> 0.8.8 bump in #1385). The command `timeout` is only enforced
server-side, so it doesn't fire.
TimeoutLangSmithSandbox drives a non-blocking CommandHandle and kills the
command if it overruns its timeout by a grace window
(SANDBOX_EXECUTE_CLIENT_GRACE_SECONDS, default 30), returning a timed-out
tool result instead of hanging. WS connect failures fall back to the base
wait=True path, whose HTTP fallback carries its own request deadline.
* fix: fall back to HTTP when WS execute connect fails
run(wait=False) eagerly opens the WebSocket and reads the "started" frame,
so connect/setup failures (and connect timeouts) raise from the run() call
itself, not from handle.result. The previous structure left run() outside
the try, so those failures bypassed the HTTP fallback and would fail every
sandbox command in any environment where the WS path is unavailable.
Move handle creation inside the fallback handler in both execute and
aexecute, and run it via to_thread in the async path since it now blocks on
connect. Add tests for connect-failure and connect-timeout fallback.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-08 11:16:08 -07:00
|
|
|
class TimeoutLangSmithSandbox(LangSmithSandbox):
|
|
|
|
|
"""LangSmith backend that enforces a client-side execution deadline.
|
|
|
|
|
|
|
|
|
|
The langsmith SDK's default execute path is now a WebSocket stream with no
|
|
|
|
|
client-side read deadline: on a live socket where the dataplane never emits
|
|
|
|
|
an exit/error frame, ``CommandHandle.result`` blocks forever and wedges the
|
|
|
|
|
run (the blocking call sits in a thread that cancellation can't reclaim).
|
|
|
|
|
|
|
|
|
|
We drive a non-blocking ``CommandHandle`` ourselves and, if the command
|
|
|
|
|
overruns its own timeout by the grace window, kill it and surface a
|
|
|
|
|
timed-out tool result instead of hanging the graph. WebSocket connect
|
|
|
|
|
failures fall back to the base wait=True path, whose HTTP fallback carries
|
|
|
|
|
its own request deadline.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
_WS_FALLBACK_ERRORS = (
|
|
|
|
|
SandboxConnectionError,
|
|
|
|
|
SandboxServerReloadError,
|
|
|
|
|
ImportError,
|
|
|
|
|
OSError,
|
|
|
|
|
TypeError,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def _deadline(self, effective_timeout: int) -> int:
|
|
|
|
|
return effective_timeout + _execute_client_grace_seconds()
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _result_to_response(result: Any) -> ExecuteResponse:
|
|
|
|
|
output = result.stdout or ""
|
|
|
|
|
if result.stderr:
|
|
|
|
|
output += "\n" + result.stderr if output else result.stderr
|
|
|
|
|
return ExecuteResponse(output=output, exit_code=result.exit_code, truncated=False)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _timeout_response(seconds: int, *, server_side: bool) -> ExecuteResponse:
|
|
|
|
|
where = "on the sandbox" if server_side else "by the client and killed"
|
|
|
|
|
return ExecuteResponse(
|
|
|
|
|
output=f"Command timed out after {seconds}s {where}.",
|
|
|
|
|
exit_code=124,
|
|
|
|
|
truncated=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
def _safe_kill(handle: Any) -> None:
|
|
|
|
|
try:
|
|
|
|
|
handle.kill()
|
|
|
|
|
except Exception: # noqa: BLE001 - best-effort cleanup of a wedged command
|
|
|
|
|
logger.warning("Failed to kill timed-out sandbox command", exc_info=True)
|
|
|
|
|
|
|
|
|
|
def _base_execute(self, command: str, timeout: int | None) -> ExecuteResponse:
|
|
|
|
|
# WS path unavailable; the base wait=True path falls back to HTTP,
|
|
|
|
|
# which carries its own request deadline.
|
|
|
|
|
return LangSmithSandbox.execute(self, command, timeout=timeout)
|
|
|
|
|
|
|
|
|
|
def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse:
|
|
|
|
|
effective = timeout if timeout is not None else self._default_timeout
|
|
|
|
|
if not effective: # 0 / None: caller opted out of any deadline
|
|
|
|
|
return super().execute(command, timeout=timeout)
|
|
|
|
|
# run(wait=False) eagerly opens the WS and reads the "started" frame, so
|
|
|
|
|
# connect/setup failures raise here — fall back to the base path.
|
|
|
|
|
try:
|
|
|
|
|
handle = self._sandbox.run(command, timeout=effective, wait=False)
|
|
|
|
|
except (*self._WS_FALLBACK_ERRORS, TimeoutError):
|
|
|
|
|
return self._base_execute(command, timeout)
|
|
|
|
|
deadline = self._deadline(effective)
|
|
|
|
|
pool = ThreadPoolExecutor(max_workers=1, thread_name_prefix="sbx-exec")
|
|
|
|
|
try:
|
|
|
|
|
future = pool.submit(lambda: handle.result)
|
|
|
|
|
try:
|
|
|
|
|
result = future.result(timeout=deadline)
|
|
|
|
|
except FuturesTimeout:
|
|
|
|
|
self._safe_kill(handle)
|
|
|
|
|
return self._timeout_response(deadline, server_side=False)
|
|
|
|
|
except CommandTimeoutError:
|
|
|
|
|
return self._timeout_response(effective, server_side=True)
|
|
|
|
|
except self._WS_FALLBACK_ERRORS:
|
|
|
|
|
return self._base_execute(command, timeout)
|
|
|
|
|
return self._result_to_response(result)
|
|
|
|
|
finally:
|
|
|
|
|
# Never join: a still-wedged worker must not block the caller.
|
|
|
|
|
pool.shutdown(wait=False)
|
|
|
|
|
|
|
|
|
|
async def aexecute(
|
|
|
|
|
self,
|
|
|
|
|
command: str,
|
|
|
|
|
*,
|
|
|
|
|
timeout: int | None = None, # noqa: ASYNC109 - forwarded semantic timeout, not an asyncio contract
|
|
|
|
|
) -> ExecuteResponse:
|
|
|
|
|
effective = timeout if timeout is not None else self._default_timeout
|
|
|
|
|
if not effective:
|
|
|
|
|
return await super().aexecute(command, timeout=timeout)
|
|
|
|
|
# run(wait=False) eagerly opens the WS and reads the "started" frame
|
|
|
|
|
# (blocking, bounded by the SDK connect timeout); connect/setup failures
|
|
|
|
|
# raise here — fall back to the base path.
|
|
|
|
|
try:
|
|
|
|
|
handle = await asyncio.to_thread(
|
|
|
|
|
self._sandbox.run, command, timeout=effective, wait=False
|
|
|
|
|
)
|
|
|
|
|
except (*self._WS_FALLBACK_ERRORS, TimeoutError):
|
|
|
|
|
return await asyncio.to_thread(self._base_execute, command, timeout)
|
|
|
|
|
deadline = self._deadline(effective)
|
|
|
|
|
try:
|
|
|
|
|
result = await asyncio.wait_for(
|
|
|
|
|
asyncio.to_thread(lambda: handle.result), timeout=deadline
|
|
|
|
|
)
|
|
|
|
|
except TimeoutError:
|
|
|
|
|
await asyncio.to_thread(self._safe_kill, handle)
|
|
|
|
|
return self._timeout_response(deadline, server_side=False)
|
|
|
|
|
except CommandTimeoutError:
|
|
|
|
|
return self._timeout_response(effective, server_side=True)
|
|
|
|
|
except self._WS_FALLBACK_ERRORS:
|
|
|
|
|
return await asyncio.to_thread(self._base_execute, command, timeout)
|
|
|
|
|
return self._result_to_response(result)
|
|
|
|
|
|
|
|
|
|
|
2026-02-10 13:35:50 -08:00
|
|
|
class SandboxProvider(ABC):
|
|
|
|
|
"""Interface for creating and deleting sandbox backends."""
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
def get_or_create(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
sandbox_id: str | None = None,
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> SandboxBackendProtocol:
|
|
|
|
|
"""Get an existing sandbox, or create one if needed."""
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
|
def delete(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
sandbox_id: str,
|
|
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Delete a sandbox by id."""
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LangSmithProvider(SandboxProvider):
|
2026-04-06 10:24:50 -07:00
|
|
|
"""LangSmith sandbox provider implementation."""
|
2026-02-10 13:35:50 -08:00
|
|
|
|
|
|
|
|
def __init__(self, api_key: str | None = None) -> None:
|
|
|
|
|
from langsmith import sandbox
|
|
|
|
|
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
self._api_key = api_key or _get_langsmith_api_key()
|
2026-02-10 13:35:50 -08:00
|
|
|
if not self._api_key:
|
fix: proxy config restored the branch yogesh/GitHub auth proxy (#1173)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
2026-04-08 15:02:52 -07:00
|
|
|
msg = "LANGSMITH_API_KEY (or LANGSMITH_API_KEY_PROD) not set"
|
2026-02-10 13:35:50 -08:00
|
|
|
raise ValueError(msg)
|
|
|
|
|
self._client: SandboxClient = sandbox.SandboxClient(api_key=self._api_key)
|
|
|
|
|
|
2026-04-21 15:17:19 -04:00
|
|
|
@classmethod
|
|
|
|
|
def validate_startup_config(cls) -> None:
|
|
|
|
|
"""Validate env-var configuration at server startup. Raises ValueError if invalid."""
|
|
|
|
|
if not os.environ.get("DEFAULT_SANDBOX_SNAPSHOT_ID"):
|
|
|
|
|
msg = "DEFAULT_SANDBOX_SNAPSHOT_ID must be set when SANDBOX_TYPE=langsmith"
|
|
|
|
|
raise ValueError(msg)
|
2026-05-08 00:30:14 -04:00
|
|
|
for name in (
|
|
|
|
|
"DEFAULT_SANDBOX_SNAPSHOT_FS_CAPACITY_BYTES",
|
|
|
|
|
"DEFAULT_SANDBOX_VCPUS",
|
|
|
|
|
"DEFAULT_SANDBOX_MEM_BYTES",
|
|
|
|
|
"DEFAULT_SANDBOX_IDLE_TTL_SECONDS",
|
|
|
|
|
"DEFAULT_SANDBOX_DELETE_AFTER_STOP_SECONDS",
|
|
|
|
|
):
|
|
|
|
|
raw = os.environ.get(name)
|
|
|
|
|
if raw is None or raw == "":
|
|
|
|
|
continue
|
2026-04-21 15:17:19 -04:00
|
|
|
try:
|
2026-05-08 00:30:14 -04:00
|
|
|
value = int(raw)
|
2026-04-21 15:17:19 -04:00
|
|
|
except ValueError as e:
|
2026-05-08 00:30:14 -04:00
|
|
|
msg = f"{name} must be an integer, got {raw!r}"
|
2026-04-21 15:17:19 -04:00
|
|
|
raise ValueError(msg) from e
|
2026-05-08 00:30:14 -04:00
|
|
|
if (
|
|
|
|
|
name
|
|
|
|
|
in {
|
|
|
|
|
"DEFAULT_SANDBOX_IDLE_TTL_SECONDS",
|
|
|
|
|
"DEFAULT_SANDBOX_DELETE_AFTER_STOP_SECONDS",
|
|
|
|
|
}
|
|
|
|
|
and value < 0
|
|
|
|
|
):
|
|
|
|
|
msg = f"{name} must be >= 0, got {value}"
|
|
|
|
|
raise ValueError(msg)
|
2026-04-21 15:17:19 -04:00
|
|
|
|
2026-02-10 13:35:50 -08:00
|
|
|
def get_or_create(
|
|
|
|
|
self,
|
|
|
|
|
*,
|
|
|
|
|
sandbox_id: str | None = None,
|
|
|
|
|
timeout: int = 180,
|
2026-04-21 15:17:19 -04:00
|
|
|
snapshot_id: str | None = None,
|
|
|
|
|
fs_capacity_bytes: int | None = None,
|
|
|
|
|
vcpus: int | None = None,
|
|
|
|
|
mem_bytes: int | None = None,
|
2026-05-08 00:30:14 -04:00
|
|
|
idle_ttl_seconds: int | None = None,
|
|
|
|
|
delete_after_stop_seconds: int | None = None,
|
2026-02-10 13:35:50 -08:00
|
|
|
**kwargs: Any,
|
|
|
|
|
) -> SandboxBackendProtocol:
|
|
|
|
|
"""Get existing or create new LangSmith sandbox."""
|
|
|
|
|
if kwargs:
|
|
|
|
|
msg = f"Received unsupported arguments: {list(kwargs.keys())}"
|
|
|
|
|
raise TypeError(msg)
|
|
|
|
|
if sandbox_id:
|
|
|
|
|
try:
|
|
|
|
|
sandbox = self._client.get_sandbox(name=sandbox_id)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
msg = f"Failed to connect to existing sandbox '{sandbox_id}': {e}"
|
|
|
|
|
raise RuntimeError(msg) from e
|
fix: enforce client-side deadline on sandbox execute (#1451)
* fix: enforce client-side deadline on sandbox execute
The langsmith SDK's default execute path is now a WebSocket stream with
no client-side read deadline. On a live socket where the dataplane never
emits an exit/error frame, CommandHandle.result blocks forever in an
uncancellable thread, wedging the run (introduced by the langsmith
0.8.3 -> 0.8.8 bump in #1385). The command `timeout` is only enforced
server-side, so it doesn't fire.
TimeoutLangSmithSandbox drives a non-blocking CommandHandle and kills the
command if it overruns its timeout by a grace window
(SANDBOX_EXECUTE_CLIENT_GRACE_SECONDS, default 30), returning a timed-out
tool result instead of hanging. WS connect failures fall back to the base
wait=True path, whose HTTP fallback carries its own request deadline.
* fix: fall back to HTTP when WS execute connect fails
run(wait=False) eagerly opens the WebSocket and reads the "started" frame,
so connect/setup failures (and connect timeouts) raise from the run() call
itself, not from handle.result. The previous structure left run() outside
the try, so those failures bypassed the HTTP fallback and would fail every
sandbox command in any environment where the WS path is unavailable.
Move handle creation inside the fallback handler in both execute and
aexecute, and run it via to_thread in the async path since it now blocks on
connect. Add tests for connect-failure and connect-timeout fallback.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-08 11:16:08 -07:00
|
|
|
return TimeoutLangSmithSandbox(sandbox)
|
2026-02-10 13:35:50 -08:00
|
|
|
|
2026-04-21 15:17:19 -04:00
|
|
|
if not snapshot_id:
|
|
|
|
|
msg = "DEFAULT_SANDBOX_SNAPSHOT_ID must be set when SANDBOX_TYPE=langsmith"
|
|
|
|
|
raise ValueError(msg)
|
2026-02-10 13:35:50 -08:00
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
sandbox = self._client.create_sandbox(
|
2026-04-21 15:17:19 -04:00
|
|
|
snapshot_id=snapshot_id,
|
|
|
|
|
fs_capacity_bytes=fs_capacity_bytes,
|
|
|
|
|
vcpus=vcpus,
|
|
|
|
|
mem_bytes=mem_bytes,
|
2026-05-08 00:30:14 -04:00
|
|
|
idle_ttl_seconds=idle_ttl_seconds,
|
|
|
|
|
delete_after_stop_seconds=delete_after_stop_seconds,
|
2026-04-21 15:17:19 -04:00
|
|
|
timeout=timeout,
|
2026-02-10 13:35:50 -08:00
|
|
|
)
|
|
|
|
|
except Exception as e:
|
2026-04-21 15:17:19 -04:00
|
|
|
msg = f"Failed to create sandbox from snapshot '{snapshot_id}': {e}"
|
2026-02-10 13:35:50 -08:00
|
|
|
raise RuntimeError(msg) from e
|
|
|
|
|
|
fix: enforce client-side deadline on sandbox execute (#1451)
* fix: enforce client-side deadline on sandbox execute
The langsmith SDK's default execute path is now a WebSocket stream with
no client-side read deadline. On a live socket where the dataplane never
emits an exit/error frame, CommandHandle.result blocks forever in an
uncancellable thread, wedging the run (introduced by the langsmith
0.8.3 -> 0.8.8 bump in #1385). The command `timeout` is only enforced
server-side, so it doesn't fire.
TimeoutLangSmithSandbox drives a non-blocking CommandHandle and kills the
command if it overruns its timeout by a grace window
(SANDBOX_EXECUTE_CLIENT_GRACE_SECONDS, default 30), returning a timed-out
tool result instead of hanging. WS connect failures fall back to the base
wait=True path, whose HTTP fallback carries its own request deadline.
* fix: fall back to HTTP when WS execute connect fails
run(wait=False) eagerly opens the WebSocket and reads the "started" frame,
so connect/setup failures (and connect timeouts) raise from the run() call
itself, not from handle.result. The previous structure left run() outside
the try, so those failures bypassed the HTTP fallback and would fail every
sandbox command in any environment where the WS path is unavailable.
Move handle creation inside the fallback handler in both execute and
aexecute, and run it via to_thread in the async path since it now blocks on
connect. Add tests for connect-failure and connect-timeout fallback.
---------
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-08 11:16:08 -07:00
|
|
|
return TimeoutLangSmithSandbox(sandbox)
|
2026-02-10 13:35:50 -08:00
|
|
|
|
2026-02-10 17:30:45 -08:00
|
|
|
def delete(self, *, sandbox_id: str, **kwargs: Any) -> None:
|
2026-02-10 13:35:50 -08:00
|
|
|
"""Delete a LangSmith sandbox."""
|
|
|
|
|
self._client.delete_sandbox(sandbox_id)
|