diff --git a/libs/code/deepagents_code/server_graph.py b/libs/code/deepagents_code/server_graph.py index b6f8ad613e..ae113dbae2 100644 --- a/libs/code/deepagents_code/server_graph.py +++ b/libs/code/deepagents_code/server_graph.py @@ -40,9 +40,14 @@ if TYPE_CHECKING: from collections.abc import Awaitable, Callable, Mapping + from contextlib import AbstractContextManager from deepagents.backends.composite import CompositeBackend + EnvironmentContext = Callable[ + [Mapping[str, str] | None], AbstractContextManager[None] + ] + from deepagents_code.config import CredentialsSnapshot from deepagents_code.extensions.registry import ExtensionRegistry from deepagents_code.offload_middleware import OffloadOperation @@ -257,12 +262,6 @@ async def _make_graphs( offload operation bound to that backend. """ config = config_override or ServerConfig.from_env() - from deepagents_code.config import ( - Credentials, - _preview_dotenv_environ, - use_environment, - ) - workspace_path = ( project_context_override.user_cwd if project_context_override is not None @@ -278,15 +277,25 @@ async def _make_graphs( # rejects when invoked directly from the server loop (see issue #5043), # for the same reason as the offload in `_make_graphs_in_environment`. def _resolve_workspace_environment() -> tuple[ - Mapping[str, str], CredentialsSnapshot + Mapping[str, str], CredentialsSnapshot, EnvironmentContext ]: + from deepagents_code.config import ( + Credentials, + _preview_dotenv_environ, + use_environment, + ) + environ = MappingProxyType(_preview_dotenv_environ(start_path=workspace_path)) - return environ, Credentials.snapshot_from_environment( - start_path=workspace_path, - environ=environ, + return ( + environ, + Credentials.snapshot_from_environment( + start_path=workspace_path, + environ=environ, + ), + use_environment, ) - workspace_env, workspace_credentials = await asyncio.to_thread( + workspace_env, workspace_credentials, use_environment = await asyncio.to_thread( _resolve_workspace_environment ) diff --git a/libs/code/tests/unit_tests/test_server_graph.py b/libs/code/tests/unit_tests/test_server_graph.py index 46ee1e6b54..5932d79e83 100644 --- a/libs/code/tests/unit_tests/test_server_graph.py +++ b/libs/code/tests/unit_tests/test_server_graph.py @@ -4,9 +4,10 @@ import importlib import os +import subprocess import sys from types import ModuleType, SimpleNamespace -from typing import Any +from typing import TYPE_CHECKING, Any from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest @@ -14,6 +15,9 @@ from deepagents_code._env_vars import SERVER_ENV_PREFIX from deepagents_code._server_config import ServerConfig +if TYPE_CHECKING: + from pathlib import Path + @pytest.fixture(autouse=True) def _disable_extensions(monkeypatch: pytest.MonkeyPatch) -> None: @@ -84,6 +88,47 @@ async def build() -> object: assert calls == 1 assert results == [graph_obj, graph_obj, graph_obj] + def test_config_bootstrap_runs_off_the_blockbuster_loop( + self, tmp_path: Path + ) -> None: + """Profile validation must not block the server event loop.""" + profile = tmp_path / "profile" + profile.mkdir() + env = os.environ.copy() + env["DEEPAGENTS_HOME"] = str(profile) + env.pop("DEEPAGENTS_HOME_IS_DEFAULT", None) + code = """ +import asyncio +from unittest.mock import AsyncMock, patch +from blockbuster import blockbuster_ctx +from deepagents_code._server_config import ServerConfig +import deepagents_code.server_graph as module + +async def main(): + runtime = module.ServerRuntime(object(), object(), object()) + with patch.object( + module, + "_make_graphs_in_environment", + new=AsyncMock(return_value=runtime), + ): + with blockbuster_ctx(): + assert await module._make_graphs( + config_override=ServerConfig(no_mcp=True) + ) is runtime + +asyncio.run(main()) +""" + + process = subprocess.run( + [sys.executable, "-c", code], + env=env, + check=False, + capture_output=True, + text=True, + ) + + assert process.returncode == 0, process.stderr + def test_criteria_context_tools_use_identity_allowlist_in_tool_order(self) -> None: """Criteria tools should be known context objects in main-tool order.""" module = _import_fresh_server_graph()