Revert "feat: authenticate git operations via sandbox proxy instead of creden…" (#1170)

This reverts commit 6305e13dc6.
This commit is contained in:
Aran Yogesh 2026-04-07 18:45:33 -07:00 • committed by GitHub
parent c5ba4e2e93
commit 9aba4d0545
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 144 additions and 294 deletions

View file

@ -2,21 +2,16 @@
from __future__ import annotations
import base64
import contextlib
import logging
import os
import time
from abc import ABC, abstractmethod
from typing import Any
import httpx
from deepagents.backends import LangSmithSandbox
from deepagents.backends.protocol import SandboxBackendProtocol
from langsmith.sandbox import SandboxClient, SandboxTemplate
logger = logging.getLogger(__name__)
def _get_langsmith_api_key() -> str | None:
"""Get LangSmith API key from environment.
@ -34,62 +29,14 @@ def _get_sandbox_template_config() -> tuple[str | None, str | None]:
return template_name, template_image
def _configure_github_proxy(sandbox_name: str, github_token: str) -> None:
"""Configure sandbox proxy to inject GitHub auth for all github.com requests.
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}"
basic_auth = base64.b64encode(f"x-access-token:{github_token}".encode()).decode()
payload = {
"proxy_config": {
"rules": [
{
"name": "github",
"match_hosts": ["github.com", "*.github.com"],
"headers": [
{
"name": "Authorization",
"type": "opaque",
"value": f"Basic {basic_auth}",
}
],
}
]
}
}
with httpx.Client() as client:
response = client.patch(
url,
json=payload,
headers={"X-API-Key": api_key},
)
response.raise_for_status()
logger.info("Configured GitHub proxy for sandbox %s", sandbox_name)
def create_langsmith_sandbox(
sandbox_id: str | None = None,
github_token: str | None = None,
) -> SandboxBackendProtocol:
"""Create or connect to a LangSmith sandbox without automatic cleanup.
Args:
sandbox_id: Optional existing sandbox ID to connect to.
If None, creates a new sandbox.
github_token: Optional GitHub token. Used to configure proxy auth on
new sandboxes. Ignored when connecting to an existing sandbox.
Returns:
SandboxBackendProtocol instance
@ -104,10 +51,6 @@ def create_langsmith_sandbox(
template_image=template_image,
)
_update_thread_sandbox_metadata(backend.id)
if sandbox_id is None and github_token:
_configure_github_proxy(backend.id, github_token)
return backend
@ -165,7 +108,7 @@ class SandboxProvider(ABC):
raise NotImplementedError
DEFAULT_TEMPLATE_NAME = "open-swe-new"
DEFAULT_TEMPLATE_NAME = "open-swe"
DEFAULT_TEMPLATE_IMAGE = "python:3"
@ -175,9 +118,9 @@ class LangSmithProvider(SandboxProvider):
def __init__(self, api_key: str | None = None) -> None:
from langsmith import sandbox
self._api_key = api_key or _get_langsmith_api_key()
self._api_key = api_key or os.environ.get("LANGSMITH_API_KEY")
if not self._api_key:
msg = "LANGSMITH_API_KEY (or LANGSMITH_API_KEY_PROD) not set"
msg = "LANGSMITH_API_KEY environment variable not set"
raise ValueError(msg)
self._client: SandboxClient = sandbox.SandboxClient(api_key=self._api_key)

View file

@ -160,7 +160,9 @@ async def open_pr_if_needed(
await asyncio.to_thread(git_add_all, sandbox_backend, repo_dir)
await asyncio.to_thread(git_commit, sandbox_backend, repo_dir, commit_message)
await asyncio.to_thread(git_push, sandbox_backend, repo_dir, target_branch)
await asyncio.to_thread(
git_push, sandbox_backend, repo_dir, target_branch, installation_token
)
base_branch = await get_github_default_branch(repo_owner, repo_name, installation_token)
logger.info("Using base branch: %s", base_branch)

View file

@ -27,7 +27,6 @@ from deepagents import create_deep_agent
from deepagents.backends.protocol import SandboxBackendProtocol
from langsmith.sandbox import SandboxClientError
from .integrations.langsmith import _configure_github_proxy
from .middleware import (
ToolErrorMiddleware,
check_message_queue_before_model,
@ -58,7 +57,6 @@ from .tools import (
web_search,
)
from .utils.auth import resolve_github_token
from .utils.github_app import get_github_app_installation_token
from .utils.model import make_model
from .utils.sandbox import create_sandbox
@ -70,28 +68,32 @@ SANDBOX_POLL_INTERVAL = 1.0
from .utils.agents_md import read_agents_md_in_sandbox
from .utils.github import (
_CRED_FILE_PATH,
cleanup_git_credentials,
git_current_branch,
git_has_uncommitted_changes,
git_pull_branch,
is_valid_git_repo,
remove_directory,
setup_git_credentials,
)
from .utils.sandbox_paths import aresolve_repo_dir, aresolve_sandbox_work_dir
from .utils.sandbox_state import SANDBOX_BACKENDS, get_sandbox_id_from_metadata
async def _clone_or_pull_repo_in_sandbox(
async def _clone_or_pull_repo_in_sandbox( # noqa: PLR0915
sandbox_backend: SandboxBackendProtocol,
owner: str,
repo: str,
github_token: str | None = None,
) -> str:
"""Clone a GitHub repo into the sandbox, or pull if it already exists.
Authentication is handled by the sandbox proxy (configured at sandbox creation
time), so no token is needed here.
Args:
sandbox_backend: The sandbox backend to execute commands in
owner: GitHub repo owner
repo: GitHub repo name
github_token: GitHub access token (from agent auth or env var)
Returns:
Path to the cloned/updated repo directory
@ -99,9 +101,18 @@ async def _clone_or_pull_repo_in_sandbox(
logger.info("_clone_or_pull_repo_in_sandbox called for %s/%s", owner, repo)
loop = asyncio.get_event_loop()
token = github_token
if not token:
msg = "No GitHub token provided"
logger.error(msg)
raise ValueError(msg)
work_dir = await aresolve_sandbox_work_dir(sandbox_backend)
repo_dir = await aresolve_repo_dir(sandbox_backend, repo)
clean_url = f"https://github.com/{owner}/{repo}.git"
cred_helper = shlex.quote(f"store --file={_CRED_FILE_PATH}")
safe_repo_dir = shlex.quote(repo_dir)
safe_clean_url = shlex.quote(clean_url)
logger.info("Resolved sandbox work dir to %s", work_dir)
@ -132,10 +143,21 @@ async def _clone_or_pull_repo_in_sandbox(
logger.info("Repo is clean, pulling latest changes from %s/%s", owner, repo)
try:
current_branch = await loop.run_in_executor(
None, git_current_branch, sandbox_backend, repo_dir
)
if not current_branch:
msg = f"Failed to determine current branch for repo at {repo_dir}"
logger.error(msg)
raise RuntimeError(msg)
pull_result = await loop.run_in_executor(
None,
sandbox_backend.execute,
f"cd {shlex.quote(repo_dir)} && git pull origin $(git rev-parse --abbrev-ref HEAD)",
git_pull_branch,
sandbox_backend,
repo_dir,
current_branch,
token,
)
logger.debug("Git pull result: exit_code=%s", pull_result.exit_code)
if pull_result.exit_code != 0:
@ -152,16 +174,19 @@ async def _clone_or_pull_repo_in_sandbox(
return repo_dir
logger.info("Cloning repo %s/%s to %s", owner, repo, repo_dir)
await loop.run_in_executor(None, setup_git_credentials, sandbox_backend, token)
try:
result = await loop.run_in_executor(
None,
sandbox_backend.execute,
f"git clone {shlex.quote(clean_url)} {shlex.quote(repo_dir)}",
f"git -c credential.helper={cred_helper} clone {safe_clean_url} {safe_repo_dir}",
)
logger.debug("Git clone result: exit_code=%s", result.exit_code)
except Exception:
logger.exception("Failed to execute git clone")
raise
finally:
await loop.run_in_executor(None, cleanup_git_credentials, sandbox_backend)
if result.exit_code != 0:
msg = f"Failed to clone repo {owner}/{repo}: {result.output}"
@ -172,35 +197,17 @@ async def _clone_or_pull_repo_in_sandbox(
return repo_dir
async def _create_sandbox_with_proxy() -> SandboxBackendProtocol:
"""Create a new sandbox with GitHub proxy auth configured.
Uses create_sandbox (generic factory) so non-langsmith providers still work.
For langsmith sandboxes, configures the proxy with the installation token.
"""
sandbox_backend = await asyncio.to_thread(create_sandbox)
sandbox_type = os.getenv("SANDBOX_TYPE", "langsmith")
if sandbox_type == "langsmith":
installation_token = await get_github_app_installation_token()
if not installation_token:
msg = "Cannot configure proxy: GitHub App installation token is unavailable"
logger.error(msg)
raise ValueError(msg)
await asyncio.to_thread(_configure_github_proxy, sandbox_backend.id, installation_token)
return sandbox_backend
async def _recreate_sandbox(
thread_id: str,
repo_owner: str,
repo_name: str,
*,
github_token: str | None,
) -> tuple[SandboxBackendProtocol, str]:
"""Recreate a sandbox and clone the repo after a connection failure.
Clears the stale cache entry, sets the SANDBOX_CREATING sentinel,
creates a fresh sandbox (with proxy auth configured), and clones the repo.
creates a fresh sandbox, and clones the repo.
"""
SANDBOX_BACKENDS.pop(thread_id, None)
await client.threads.update(
@ -208,8 +215,10 @@ async def _recreate_sandbox(
metadata={"sandbox_id": SANDBOX_CREATING},
)
try:
sandbox_backend = await _create_sandbox_with_proxy()
repo_dir = await _clone_or_pull_repo_in_sandbox(sandbox_backend, repo_owner, repo_name)
sandbox_backend = await asyncio.to_thread(create_sandbox)
repo_dir = await _clone_or_pull_repo_in_sandbox(
sandbox_backend, repo_owner, repo_name, github_token
)
except Exception:
logger.exception("Failed to recreate sandbox after connection failure")
await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None})
@ -287,7 +296,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
logger.info("Pulling latest changes for repo %s/%s", repo_owner, repo_name)
try:
repo_dir = await _clone_or_pull_repo_in_sandbox(
sandbox_backend, repo_owner, repo_name
sandbox_backend, repo_owner, repo_name, github_token
)
except SandboxClientError:
logger.warning(
@ -295,7 +304,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
thread_id,
)
sandbox_backend, repo_dir = await _recreate_sandbox(
thread_id, repo_owner, repo_name
thread_id, repo_owner, repo_name, github_token=github_token
)
except Exception:
logger.exception("Failed to pull repo in cached sandbox")
@ -306,14 +315,15 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": SANDBOX_CREATING})
try:
sandbox_backend = await _create_sandbox_with_proxy()
# Create sandbox without context manager cleanup (sandbox persists)
sandbox_backend = await asyncio.to_thread(create_sandbox)
logger.info("Sandbox created: %s", sandbox_backend.id)
repo_dir = None
if repo_owner and repo_name:
logger.info("Cloning repo %s/%s into sandbox", repo_owner, repo_name)
repo_dir = await _clone_or_pull_repo_in_sandbox(
sandbox_backend, repo_owner, repo_name
sandbox_backend, repo_owner, repo_name, github_token
)
logger.info("Repo cloned to %s", repo_dir)
@ -332,19 +342,19 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
else:
logger.info("Connecting to existing sandbox %s", sandbox_id)
try:
# Connect to existing sandbox (no proxy reconfiguration needed)
# Connect to existing sandbox without context manager cleanup
sandbox_backend = await asyncio.to_thread(create_sandbox, sandbox_id)
logger.info("Connected to existing sandbox %s", sandbox_id)
except Exception:
logger.warning("Failed to connect to existing sandbox %s, creating new one", sandbox_id)
# Reset sandbox_id and create a new sandbox with proxy auth configured
# Reset sandbox_id and create a new sandbox
await client.threads.update(
thread_id=thread_id,
metadata={"sandbox_id": SANDBOX_CREATING},
)
try:
sandbox_backend = await _create_sandbox_with_proxy()
sandbox_backend = await asyncio.to_thread(create_sandbox)
logger.info("New sandbox created: %s", sandbox_backend.id)
except Exception:
logger.exception("Failed to create replacement sandbox")
@ -358,7 +368,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
logger.info("Pulling latest changes for repo %s/%s", repo_owner, repo_name)
try:
repo_dir = await _clone_or_pull_repo_in_sandbox(
sandbox_backend, repo_owner, repo_name
sandbox_backend, repo_owner, repo_name, github_token
)
except SandboxClientError:
logger.warning(
@ -366,7 +376,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
thread_id,
)
sandbox_backend, repo_dir = await _recreate_sandbox(
thread_id, repo_owner, repo_name
thread_id, repo_owner, repo_name, github_token=github_token
)
except Exception:
logger.exception("Failed to pull repo in existing sandbox")

View file

@ -151,6 +151,14 @@ def commit_and_open_pr(
if not (has_uncommitted_changes or has_unpushed_commits):
return {"success": False, "error": "No changes detected", "pr_url": None}
installation_token = asyncio.run(get_github_app_installation_token())
if not installation_token:
return {
"success": False,
"error": "Failed to get GitHub App installation token",
"pr_url": None,
}
metadata = config.get("metadata", {})
branch_name = metadata.get("branch_name")
current_branch = git_current_branch(sandbox_backend, repo_dir)
@ -190,15 +198,7 @@ def commit_and_open_pr(
"pr_url": None,
}
installation_token = asyncio.run(get_github_app_installation_token())
if not installation_token:
return {
"success": False,
"error": "Failed to get GitHub App installation token",
"pr_url": None,
}
push_result = git_push(sandbox_backend, repo_dir, target_branch)
push_result = git_push(sandbox_backend, repo_dir, target_branch, installation_token)
if push_result.exit_code != 0:
return {
"success": False,

View file

@ -122,18 +122,66 @@ def git_get_remote_url(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -
return result.output.strip()
_CRED_FILE_PATH = "/tmp/.git-credentials"
def setup_git_credentials(sandbox_backend: SandboxBackendProtocol, github_token: str) -> None:
"""Write GitHub credentials to a temporary file using the sandbox write API.
The write API sends content in the HTTP body (not via a shell command),
so the token never appears in shell history or process listings.
"""
sandbox_backend.write(_CRED_FILE_PATH, f"https://git:{github_token}@github.com\n")
sandbox_backend.execute(f"chmod 600 {_CRED_FILE_PATH}")
def cleanup_git_credentials(sandbox_backend: SandboxBackendProtocol) -> None:
"""Remove the temporary credentials file."""
sandbox_backend.execute(f"rm -f {_CRED_FILE_PATH}")
def _git_with_credentials(
sandbox_backend: SandboxBackendProtocol,
repo_dir: str,
command: str,
) -> ExecuteResponse:
"""Run a git command using the temporary credential file."""
cred_helper = shlex.quote(f"store --file={_CRED_FILE_PATH}")
return _run_git(sandbox_backend, repo_dir, f"git -c credential.helper={cred_helper} {command}")
def git_push(
sandbox_backend: SandboxBackendProtocol,
repo_dir: str,
branch: str,
github_token: str | None = None,
) -> ExecuteResponse:
"""Push the branch to origin.
Authentication is handled by the sandbox proxy (configured at sandbox creation
time via the LangSmith proxy-config API), so no token is needed here.
"""
"""Push the branch to origin, using a token if needed."""
safe_branch = shlex.quote(branch)
return _run_git(sandbox_backend, repo_dir, f"git push origin {safe_branch}")
if not github_token:
return _run_git(sandbox_backend, repo_dir, f"git push origin {safe_branch}")
setup_git_credentials(sandbox_backend, github_token)
try:
return _git_with_credentials(sandbox_backend, repo_dir, f"push origin {safe_branch}")
finally:
cleanup_git_credentials(sandbox_backend)
def git_pull_branch(
sandbox_backend: SandboxBackendProtocol,
repo_dir: str,
branch: str,
github_token: str | None = None,
) -> ExecuteResponse:
"""Pull a specific branch from origin, using a token if needed."""
safe_branch = shlex.quote(branch)
if not github_token:
return _run_git(sandbox_backend, repo_dir, f"git pull origin {safe_branch}")
setup_git_credentials(sandbox_backend, github_token)
try:
return _git_with_credentials(sandbox_backend, repo_dir, f"pull origin {safe_branch}")
finally:
cleanup_git_credentials(sandbox_backend)
async def create_github_pr(

View file

@ -18,7 +18,7 @@ dependencies = [
"markdownify>=1.2.2",
"langchain-anthropic>1.1.0",
"langgraph-cli[inmem]>=0.4.12",
"langsmith>=0.7.20",
"langsmith>=0.7.1",
"langchain-openai==1.1.10",
"langchain-daytona>=0.0.3",
"langchain-modal>=0.0.2",

View file

@ -27,3 +27,22 @@ def test_git_checkout_existing_branch_quotes_repo_dir_and_branch() -> None:
github.git_checkout_existing_branch(sandbox, repo_dir, branch)
assert sandbox.commands == [f"cd {shlex.quote(repo_dir)} && git checkout {shlex.quote(branch)}"]
def test_git_pull_branch_quotes_repo_dir_and_branch_when_using_credentials() -> None:
sandbox = FakeSandboxBackend()
repo_dir = "/tmp/repo; curl attacker"
branch = "main; curl attacker"
github.git_pull_branch(sandbox, repo_dir, branch, github_token="secret-token")
assert sandbox.writes == [(github._CRED_FILE_PATH, "https://git:secret-token@github.com\n")]
assert sandbox.commands == [
f"chmod 600 {github._CRED_FILE_PATH}",
(
f"cd {shlex.quote(repo_dir)} && git -c "
f"credential.helper={shlex.quote(f'store --file={github._CRED_FILE_PATH}')}"
f" pull origin {shlex.quote(branch)}"
),
f"rm -f {github._CRED_FILE_PATH}",
]

View file

@ -1,172 +0,0 @@
"""Tests for GitHub proxy auth configuration."""
from __future__ import annotations
import base64
from unittest.mock import AsyncMock, MagicMock, patch
import httpx
import pytest
from agent.integrations.langsmith import _configure_github_proxy
class TestConfigureGithubProxy:
"""Tests for _configure_github_proxy payload shape and error handling."""
def test_sends_correct_payload_shape(self) -> None:
"""Verify the PATCH request uses opaque headers with correct structure."""
token = "ghs_testtoken123"
expected_basic = base64.b64encode(f"x-access-token:{token}".encode()).decode()
with (
patch("agent.integrations.langsmith.httpx.Client") as mock_client_cls,
patch.dict("os.environ", {"LANGSMITH_API_KEY": "ls-api-key"}),
):
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_client.patch.return_value = mock_response
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
_configure_github_proxy("sandbox-abc123", token)
mock_client.patch.assert_called_once()
call_kwargs = mock_client.patch.call_args
payload = call_kwargs.kwargs["json"]
# Verify proxy_config structure
assert "proxy_config" in payload
rules = payload["proxy_config"]["rules"]
assert len(rules) == 1
rule = rules[0]
assert rule["name"] == "github"
assert rule["match_hosts"] == ["github.com", "*.github.com"]
headers = rule["headers"]
assert len(headers) == 1
assert headers[0]["name"] == "Authorization"
assert headers[0]["type"] == "opaque"
assert headers[0]["value"] == f"Basic {expected_basic}"
def test_sends_to_correct_url(self) -> None:
"""Verify the PATCH hits the right endpoint."""
with (
patch("agent.integrations.langsmith.httpx.Client") as mock_client_cls,
patch.dict(
"os.environ",
{
"LANGSMITH_ENDPOINT": "https://test.api.smith.langchain.com",
"LANGSMITH_API_KEY": "api-key",
},
),
):
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_client.patch.return_value = mock_response
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
_configure_github_proxy("sandbox-xyz", "token")
url = mock_client.patch.call_args.args[0]
assert url == "https://test.api.smith.langchain.com/v2/sandboxes/boxes/sandbox-xyz"
def test_sends_api_key_header(self) -> None:
"""Verify the PATCH includes the LangSmith API key."""
with (
patch("agent.integrations.langsmith.httpx.Client") as mock_client_cls,
patch.dict("os.environ", {"LANGSMITH_API_KEY": "my-api-key"}),
):
mock_client = MagicMock()
mock_response = MagicMock()
mock_response.raise_for_status = MagicMock()
mock_client.patch.return_value = mock_response
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
_configure_github_proxy("sandbox-abc", "token")
headers = mock_client.patch.call_args.kwargs["headers"]
assert headers == {"X-API-Key": "my-api-key"}
def test_raises_on_http_error(self) -> None:
"""Verify HTTP errors propagate."""
with (
patch("agent.integrations.langsmith.httpx.Client") as mock_client_cls,
patch.dict("os.environ", {"LANGSMITH_API_KEY": "api-key"}),
):
mock_client = MagicMock()
mock_client.patch.side_effect = httpx.HTTPStatusError(
"Server error", request=MagicMock(), response=MagicMock(status_code=500)
)
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
with pytest.raises(httpx.HTTPStatusError):
_configure_github_proxy("sandbox-abc", "token")
class TestCreateSandboxWithProxy:
"""Tests for _create_sandbox_with_proxy token source selection."""
@pytest.mark.asyncio
async def test_uses_installation_token_for_langsmith(self) -> None:
"""Installation token should be used for proxy auth on langsmith sandboxes."""
with (
patch(
"agent.server.get_github_app_installation_token",
new_callable=AsyncMock,
return_value="ghs_install",
),
patch("agent.server.create_sandbox") as mock_create,
patch("agent.server._configure_github_proxy") as mock_proxy,
patch.dict("os.environ", {"SANDBOX_TYPE": "langsmith", "LANGSMITH_API_KEY": "ls-key"}),
):
mock_create.return_value = MagicMock(id="sandbox-123")
from agent.server import _create_sandbox_with_proxy
await _create_sandbox_with_proxy()
mock_create.assert_called_once_with()
mock_proxy.assert_called_once_with("sandbox-123", "ghs_install")
@pytest.mark.asyncio
async def test_skips_proxy_for_non_langsmith(self) -> None:
"""Non-langsmith sandboxes should skip proxy configuration."""
with (
patch("agent.server.create_sandbox") as mock_create,
patch("agent.server._configure_github_proxy") as mock_proxy,
patch.dict("os.environ", {"SANDBOX_TYPE": "daytona"}),
):
mock_create.return_value = MagicMock(id="sandbox-456")
from agent.server import _create_sandbox_with_proxy
await _create_sandbox_with_proxy()
mock_create.assert_called_once_with()
mock_proxy.assert_not_called()
@pytest.mark.asyncio
async def test_raises_when_no_installation_token_for_langsmith(self) -> None:
"""Should raise ValueError when installation token is unavailable for langsmith."""
with (
patch("agent.server.create_sandbox") as mock_create,
patch(
"agent.server.get_github_app_installation_token",
new_callable=AsyncMock,
return_value=None,
),
patch.dict("os.environ", {"SANDBOX_TYPE": "langsmith"}),
):
mock_create.return_value = MagicMock(id="sandbox-789")
from agent.server import _create_sandbox_with_proxy
with pytest.raises(ValueError, match="installation token is unavailable"):
await _create_sandbox_with_proxy()

8
uv.lock generated
View file

@ -1505,7 +1505,7 @@ wheels = [
[[package]]
name = "langsmith"
version = "0.7.25"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
@ -1518,9 +1518,9 @@ dependencies = [
{ name = "xxhash" },
{ name = "zstandard" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7e/d7/21ffae5ccdc3c9b8de283e8f8bf48a92039681df0d39f15133d8ff8965bd/langsmith-0.7.25.tar.gz", hash = "sha256:d17da71f156ca69eafd28ac9627c8e0e93170260ec37cd27cedc83205a067598", size = 1145410, upload-time = "2026-04-03T13:11:42.36Z" }
sdist = { url = "https://files.pythonhosted.org/packages/67/48/3151de6df96e0977b8d319b03905e29db0df6929a85df1d922a030b7e68d/langsmith-0.7.1.tar.gz", hash = "sha256:e3fec2f97f7c5192f192f4873d6a076b8c6469768022323dded07087d8cb70a4", size = 984367, upload-time = "2026-02-10T01:55:24.696Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/13/67889d41baf7dbaf13ffd0b334a0f284e107fad1cc8782a1abb1e56e5eeb/langsmith-0.7.25-py3-none-any.whl", hash = "sha256:55ecc24c547f6c79b5a684ff8685c669eec34e52fcac5d2c0af7d613aef5a632", size = 359417, upload-time = "2026-04-03T13:11:40.729Z" },
{ url = "https://files.pythonhosted.org/packages/ce/87/6f2b008a456b4f5fd0fb1509bb7e1e9368c1a0c9641a535f224a9ddc10f3/langsmith-0.7.1-py3-none-any.whl", hash = "sha256:92cfa54253d35417184c297ad25bfd921d95f15d60a1ca75f14d4e7acd152a29", size = 322515, upload-time = "2026-02-10T01:55:22.531Z" },
]
[package.optional-dependencies]
@ -1826,7 +1826,7 @@ requires-dist = [
{ name = "langgraph", specifier = ">=1.0.8" },
{ name = "langgraph-cli", extras = ["inmem"], specifier = ">=0.4.12" },
{ name = "langgraph-sdk", specifier = ">=0.1.0" },
{ name = "langsmith", specifier = ">=0.7.20" },
{ name = "langsmith", specifier = ">=0.7.1" },
{ name = "markdownify", specifier = ">=1.2.2" },
{ name = "pygments", marker = "extra == 'dev'", specifier = ">=2.20.0" },
{ name = "pyjwt", specifier = ">=2.12.0" },