fix(daytona): make sandbox snapshot configurable (#1220)

This commit is contained in:
fuyua9 2026-05-02 06:51:34 +08:00 • committed by GitHub
parent b829cef97a
commit 226e6c8263
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 81 additions and 4 deletions

View file

@ -49,7 +49,7 @@ Set the `SANDBOX_TYPE` environment variable to switch providers. Each provider h
| `SANDBOX_TYPE` | Integration file | Required env vars | | `SANDBOX_TYPE` | Integration file | Required env vars |
|---|---|---| |---|---|---|
| `langsmith` (default) | `agent/integrations/langsmith.py` | `LANGSMITH_API_KEY_PROD`, `SANDBOX_TYPE="langsmith"` | | `langsmith` (default) | `agent/integrations/langsmith.py` | `LANGSMITH_API_KEY_PROD`, `SANDBOX_TYPE="langsmith"` |
| `daytona` | `agent/integrations/daytona.py` | `DAYTONA_API_KEY`, `SANDBOX_TYPE="daytona"` | | `daytona` | `agent/integrations/daytona.py` | `DAYTONA_API_KEY`, `SANDBOX_TYPE="daytona"`, optional `DAYTONA_SANDBOX_SNAPSHOT` |
| `runloop` | `agent/integrations/runloop.py` | `RUNLOOP_API_KEY`, `SANDBOX_TYPE="runloop"` | | `runloop` | `agent/integrations/runloop.py` | `RUNLOOP_API_KEY`, `SANDBOX_TYPE="runloop"` |
| `modal` | `agent/integrations/modal.py` | Modal credentials, `SANDBOX_TYPE="modal"` | | `modal` | `agent/integrations/modal.py` | Modal credentials, `SANDBOX_TYPE="modal"` |
| `local` | `agent/integrations/local.py` | None (no isolation — development only), `SANDBOX_TYPE="local"` | | `local` | `agent/integrations/local.py` | None (no isolation — development only), `SANDBOX_TYPE="local"` |

View file

@ -3,8 +3,15 @@ import os
from daytona import CreateSandboxFromSnapshotParams, Daytona, DaytonaConfig from daytona import CreateSandboxFromSnapshotParams, Daytona, DaytonaConfig
from langchain_daytona import DaytonaSandbox from langchain_daytona import DaytonaSandbox
# TODO: Update this to include your specific sandbox configuration DEFAULT_DAYTONA_SANDBOX_SNAPSHOT = "daytonaio/sandbox:0.6.0"
DAYTONA_SANDBOX_PARAMS = CreateSandboxFromSnapshotParams(snapshot="daytonaio/sandbox:0.6.0") DAYTONA_SANDBOX_SNAPSHOT_ENV = "DAYTONA_SANDBOX_SNAPSHOT"
def _get_daytona_sandbox_params() -> CreateSandboxFromSnapshotParams:
snapshot = os.getenv(DAYTONA_SANDBOX_SNAPSHOT_ENV, DEFAULT_DAYTONA_SANDBOX_SNAPSHOT).strip()
if not snapshot:
raise ValueError(f"{DAYTONA_SANDBOX_SNAPSHOT_ENV} must not be empty")
return CreateSandboxFromSnapshotParams(snapshot=snapshot)
def create_daytona_sandbox(sandbox_id: str | None = None): def create_daytona_sandbox(sandbox_id: str | None = None):
@ -17,6 +24,6 @@ def create_daytona_sandbox(sandbox_id: str | None = None):
if sandbox_id: if sandbox_id:
sandbox = daytona.get(sandbox_id) sandbox = daytona.get(sandbox_id)
else: else:
sandbox = daytona.create(params=DAYTONA_SANDBOX_PARAMS) sandbox = daytona.create(params=_get_daytona_sandbox_params())
return DaytonaSandbox(sandbox=sandbox) return DaytonaSandbox(sandbox=sandbox)

View file

@ -0,0 +1,70 @@
import importlib.util
import sys
import types
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
class _FakeCreateSandboxFromSnapshotParams:
def __init__(self, *, snapshot: str):
self.snapshot = snapshot
class _FakeDaytonaConfig:
def __init__(self, *, api_key: str):
self.api_key = api_key
class _FakeDaytonaSandbox:
def __init__(self, *, sandbox):
self.sandbox = sandbox
def _load_daytona_module(monkeypatch):
fake_daytona = types.ModuleType("daytona")
fake_daytona.CreateSandboxFromSnapshotParams = _FakeCreateSandboxFromSnapshotParams
fake_daytona.DaytonaConfig = _FakeDaytonaConfig
fake_daytona.Daytona = object
fake_langchain_daytona = types.ModuleType("langchain_daytona")
fake_langchain_daytona.DaytonaSandbox = _FakeDaytonaSandbox
monkeypatch.setitem(sys.modules, "daytona", fake_daytona)
monkeypatch.setitem(sys.modules, "langchain_daytona", fake_langchain_daytona)
module_path = ROOT / "agent" / "integrations" / "daytona.py"
spec = importlib.util.spec_from_file_location("daytona_under_test", module_path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_daytona_params_default_to_existing_snapshot(monkeypatch):
monkeypatch.delenv("DAYTONA_SANDBOX_SNAPSHOT", raising=False)
module = _load_daytona_module(monkeypatch)
params = module._get_daytona_sandbox_params()
assert params.snapshot == "daytonaio/sandbox:0.6.0"
def test_daytona_params_use_env_snapshot(monkeypatch):
monkeypatch.setenv("DAYTONA_SANDBOX_SNAPSHOT", "custom/snapshot:1.0")
module = _load_daytona_module(monkeypatch)
params = module._get_daytona_sandbox_params()
assert params.snapshot == "custom/snapshot:1.0"
def test_daytona_params_reject_empty_snapshot(monkeypatch):
monkeypatch.setenv("DAYTONA_SANDBOX_SNAPSHOT", " ")
module = _load_daytona_module(monkeypatch)
try:
module._get_daytona_sandbox_params()
except ValueError as exc:
assert "DAYTONA_SANDBOX_SNAPSHOT must not be empty" in str(exc)
else:
raise AssertionError("expected empty Daytona snapshot to fail")