mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 18:33:15 +00:00
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:
parent
67c782c295
commit
91f63de361
2 changed files with 140 additions and 1 deletions
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue