fix: refresh GitHub proxy token on sandbox reuse to prevent git auth failures (#1178)

* fix: refresh GitHub proxy token on sandbox reuse to prevent git auth failures

* fix: refresh GitHub proxy token on sandbox reuse to prevent git auth failures

* function name change
This commit is contained in:
Aran Yogesh 2026-04-09 14:59:49 -07:00 • committed by GitHub
parent 67c782c295
commit 91f63de361
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 140 additions and 1 deletions

View file

@ -192,6 +192,24 @@ async def _create_sandbox_with_proxy() -> SandboxBackendProtocol:
return sandbox_backend
async def _refresh_github_proxy(
sandbox_backend: SandboxBackendProtocol,
) -> None:
"""Refresh GitHub proxy credentials for reused LangSmith sandboxes."""
if os.getenv("SANDBOX_TYPE", "langsmith") != "langsmith":
return
installation_token = await get_github_app_installation_token()
if not installation_token:
logger.warning(
"Skipping GitHub proxy refresh for sandbox %s: installation token unavailable",
sandbox_backend.id,
)
return
await asyncio.to_thread(_configure_github_proxy, sandbox_backend.id, installation_token)
async def _recreate_sandbox(
thread_id: str,
repo_owner: str,
@ -283,6 +301,8 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
metadata = get_config().get("metadata", {})
repo_dir = metadata.get("repo_dir")
await _refresh_github_proxy(sandbox_backend)
if repo_owner and repo_name:
logger.info("Pulling latest changes for repo %s/%s", repo_owner, repo_name)
try:
@ -332,7 +352,6 @@ 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)
sandbox_backend = await asyncio.to_thread(create_sandbox, sandbox_id)
logger.info("Connected to existing sandbox %s", sandbox_id)
except Exception:
@ -351,6 +370,8 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None})
raise
await _refresh_github_proxy(sandbox_backend)
metadata = get_config().get("metadata", {})
repo_dir = metadata.get("repo_dir")

View file

@ -170,3 +170,121 @@ class TestCreateSandboxWithProxy:
with pytest.raises(ValueError, match="installation token is unavailable"):
await _create_sandbox_with_proxy()
class _DummyAgent:
def with_config(self, config):
return self
class TestRefreshProxyOnSandboxReuse:
"""Tests for refreshing GitHub proxy auth on sandbox reuse."""
@staticmethod
def _execution_config() -> dict:
return {
"configurable": {
"__is_for_execution__": True,
"thread_id": "thread-123",
"repo": {"owner": "langchain-ai", "name": "open-swe"},
},
"metadata": {},
}
@pytest.mark.asyncio
async def test_refreshes_proxy_for_cached_langsmith_sandbox(self) -> None:
"""Cached sandboxes should get a fresh proxy token before git operations."""
config = self._execution_config()
mock_sandbox = MagicMock(id="sandbox-cached")
with (
patch("agent.server.get_config", return_value=config),
patch(
"agent.server.resolve_github_token",
new_callable=AsyncMock,
return_value=("ghp", "enc"),
),
patch(
"agent.server.get_sandbox_id_from_metadata",
new_callable=AsyncMock,
return_value="sandbox-cached",
),
patch(
"agent.server.get_github_app_installation_token",
new_callable=AsyncMock,
return_value="ghs_fresh",
),
patch("agent.server._configure_github_proxy") as mock_proxy,
patch(
"agent.server._clone_or_pull_repo_in_sandbox",
new_callable=AsyncMock,
return_value="/workspace/open-swe",
),
patch(
"agent.server.read_agents_md_in_sandbox",
new_callable=AsyncMock,
return_value="",
),
patch("agent.server.make_model", return_value=MagicMock()),
patch("agent.server.construct_system_prompt", return_value="prompt"),
patch("agent.server.create_deep_agent", return_value=_DummyAgent()),
patch.dict(
"agent.server.SANDBOX_BACKENDS",
{"thread-123": mock_sandbox},
clear=True,
),
patch.dict("os.environ", {"SANDBOX_TYPE": "langsmith"}),
):
from agent.server import get_agent
await get_agent(config)
mock_proxy.assert_called_once_with("sandbox-cached", "ghs_fresh")
@pytest.mark.asyncio
async def test_refreshes_proxy_when_reconnecting_to_existing_langsmith_sandbox(self) -> None:
"""Reconnected sandboxes should also get a fresh proxy token."""
config = self._execution_config()
mock_sandbox = MagicMock(id="sandbox-existing")
with (
patch("agent.server.get_config", return_value=config),
patch(
"agent.server.resolve_github_token",
new_callable=AsyncMock,
return_value=("ghp", "enc"),
),
patch(
"agent.server.get_sandbox_id_from_metadata",
new_callable=AsyncMock,
return_value="sandbox-existing",
),
patch("agent.server.create_sandbox", return_value=mock_sandbox) as mock_create,
patch(
"agent.server.get_github_app_installation_token",
new_callable=AsyncMock,
return_value="ghs_fresh",
),
patch("agent.server._configure_github_proxy") as mock_proxy,
patch(
"agent.server._clone_or_pull_repo_in_sandbox",
new_callable=AsyncMock,
return_value="/workspace/open-swe",
),
patch(
"agent.server.read_agents_md_in_sandbox",
new_callable=AsyncMock,
return_value="",
),
patch("agent.server.make_model", return_value=MagicMock()),
patch("agent.server.construct_system_prompt", return_value="prompt"),
patch("agent.server.create_deep_agent", return_value=_DummyAgent()),
patch.dict("agent.server.SANDBOX_BACKENDS", {}, clear=True),
patch.dict("os.environ", {"SANDBOX_TYPE": "langsmith"}),
):
from agent.server import get_agent
await get_agent(config)
mock_create.assert_called_once_with("sandbox-existing")
mock_proxy.assert_called_once_with("sandbox-existing", "ghs_fresh")