From d8a9d6755d9b3bdbf495b6ad22bde1510790c66d Mon Sep 17 00:00:00 2001 From: Alexander Olsen <13665641+aolsenjazz@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:33:24 +0000 Subject: [PATCH 1/4] feat(code): make Auto mode generally available Co-authored-by: open-swe[bot] --- libs/code/deepagents_code/agent.py | 6 ------ libs/code/deepagents_code/app.py | 10 +++------ libs/code/deepagents_code/main.py | 21 +++++++------------ libs/code/deepagents_code/server_graph.py | 8 +------ .../deepagents_code/tui/widgets/approval.py | 9 ++++---- libs/code/tests/unit_tests/test_agent.py | 18 +++------------- libs/code/tests/unit_tests/test_app.py | 18 +++++++--------- libs/code/tests/unit_tests/test_main_args.py | 2 +- .../tests/unit_tests/test_server_graph.py | 2 +- 9 files changed, 29 insertions(+), 65 deletions(-) diff --git a/libs/code/deepagents_code/agent.py b/libs/code/deepagents_code/agent.py index 9f8f0909278..4012a6d149f 100644 --- a/libs/code/deepagents_code/agent.py +++ b/libs/code/deepagents_code/agent.py @@ -1971,12 +1971,6 @@ def create_cli_agent( """ tools = tools or [] mcp_tools = tuple(mcp_tools or ()) - if auto_mode_enabled and not is_env_truthy(EXPERIMENTAL): - logger.warning( - "Classifier-backed Auto requires %s=1; using Manual HITL", - EXPERIMENTAL, - ) - auto_mode_enabled = False if auto_mode_enabled and (not interactive or sandbox is not None): logger.warning( "Classifier-backed Auto is unavailable outside the local interactive " diff --git a/libs/code/deepagents_code/app.py b/libs/code/deepagents_code/app.py index b7289175ef9..31f8d16350a 100644 --- a/libs/code/deepagents_code/app.py +++ b/libs/code/deepagents_code/app.py @@ -3023,11 +3023,7 @@ def __init__( self._sandbox_type: str | None = raw if raw and raw != "none" else None """Normalized sandbox type (or `None`), attached to trace metadata.""" - from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy - - self._auto_mode_eligible = self._sandbox_type is None and is_env_truthy( - EXPERIMENTAL - ) + self._auto_mode_eligible = self._sandbox_type is None if self._approval_mode is ApprovalMode.AUTO and not self._auto_mode_eligible: self._approval_mode = ApprovalMode.MANUAL self._auto_approve = False @@ -7434,7 +7430,7 @@ async def _on_auto_approve_enabled(self) -> bool: if not self._auto_mode_eligible: self._warn_live_approval_mode_unavailable( - "Auto is available only in the opt-in local TUI beta." + "Auto is available only in the local TUI." ) return False if not await self._write_live_approval_mode(ApprovalMode.AUTO): @@ -16292,7 +16288,7 @@ async def action_toggle_auto_approve(self) -> None: if self._approval_mode is ApprovalMode.MANUAL: if not self._auto_mode_eligible: self._warn_live_approval_mode_unavailable( - "Auto is available only in the opt-in local TUI beta." + "Auto is available only in the local TUI." ) return target = ApprovalMode.AUTO diff --git a/libs/code/deepagents_code/main.py b/libs/code/deepagents_code/main.py index 5ad654b9502..d1721e4a560 100644 --- a/libs/code/deepagents_code/main.py +++ b/libs/code/deepagents_code/main.py @@ -1991,10 +1991,7 @@ def help_parent(help_fn: Callable[[], None]) -> list[argparse.ArgumentParser]: "--auto-approve", action="store_true", default=None, - help=( - "Interactive local TUI only: enable beta classifier-backed Auto mode. " - "Requires DEEPAGENTS_CODE_EXPERIMENTAL=1." - ), + help="Interactive local TUI only: enable beta classifier-backed Auto mode.", ) approval_group.add_argument( "--yolo", @@ -4634,20 +4631,18 @@ def cli_main() -> None: # advisory as a startup notification instead (see # `DeepAgentsApp._notify_interpreter_tools_without_interpreter`). - from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy from deepagents_code.approval_mode import ApprovalMode approval_mode = _resolve_approval_mode(args) - if approval_mode is ApprovalMode.AUTO and ( - not is_env_truthy(EXPERIMENTAL) - or (args.sandbox and args.sandbox != "none") + if ( + approval_mode is ApprovalMode.AUTO + and args.sandbox + and args.sandbox != "none" ): - reason = ( - "Auto is unavailable with a sandbox" - if args.sandbox and args.sandbox != "none" - else f"Auto is an opt-in beta; set {EXPERIMENTAL}=1" + console.print( + "[yellow]Auto is unavailable with a sandbox. " + "Using Manual.[/yellow]" ) - console.print(f"[yellow]{reason}. Using Manual.[/yellow]") approval_mode = ApprovalMode.MANUAL if approval_mode is ApprovalMode.YOLO and not _ensure_yolo_acknowledged( console diff --git a/libs/code/deepagents_code/server_graph.py b/libs/code/deepagents_code/server_graph.py index 89c0683a480..42db04718d5 100644 --- a/libs/code/deepagents_code/server_graph.py +++ b/libs/code/deepagents_code/server_graph.py @@ -279,13 +279,7 @@ def _cleanup_sandbox() -> None: def _create_cli_agent_sync() -> Any: # noqa: ANN401 async_subagents = load_async_subagents() or None - from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy - - auto_mode_enabled = ( - config.interactive - and sandbox_backend is None - and is_env_truthy(EXPERIMENTAL) - ) + auto_mode_enabled = config.interactive and sandbox_backend is None # These process-global settings writes are safe here because `make_graph` # is lock-serialized and caches one graph for the server process lifetime. diff --git a/libs/code/deepagents_code/tui/widgets/approval.py b/libs/code/deepagents_code/tui/widgets/approval.py index 77b013146a9..05c6b8a5196 100644 --- a/libs/code/deepagents_code/tui/widgets/approval.py +++ b/libs/code/deepagents_code/tui/widgets/approval.py @@ -152,8 +152,8 @@ def __init__( file-operation previews. id: Optional widget ID. Defaults to 'approval-menu'. auto_mode_eligible: Whether Auto mode can be enabled in this session. - When `False` (e.g. the experimental opt-in is off), the "Enable - Auto for this thread" option is not offered. + When `False` (e.g. a sandbox is active), the "Enable Auto for this + thread" option is not offered. **kwargs: Additional keyword arguments passed to the Container base class. """ super().__init__(id=id or "approval-menu", classes="approval-menu", **kwargs) @@ -427,9 +427,8 @@ def _build_options(self) -> list[tuple[str, str]]: """Build the visible options as `(label, decision_type)` pairs. The Auto option is omitted unless Auto can actually be enabled - (`_show_auto_option`), so it is never suggested outside the - experimental opt-in. Labels are unnumbered; `_update_options` - prefixes the display number. + (`_show_auto_option`), so it is never suggested outside the local TUI. + Labels are unnumbered; `_update_options` prefixes the display number. Returns: Ordered `(label, decision_type)` pairs for the visible options. diff --git a/libs/code/tests/unit_tests/test_agent.py b/libs/code/tests/unit_tests/test_agent.py index 228abf39404..b54174e9cc1 100644 --- a/libs/code/tests/unit_tests/test_agent.py +++ b/libs/code/tests/unit_tests/test_agent.py @@ -4811,23 +4811,14 @@ def _build_mock_settings(tmp_path: Path) -> Mock: mock_settings.interpreter_ptc_acknowledge_unsafe = False return mock_settings - @pytest.mark.parametrize( - ("experimental", "expected"), [(False, False), (True, True)] - ) - def test_auto_mode_requires_experimental_flag( + def test_auto_mode_does_not_require_experimental_flag( self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, - *, - experimental: bool, - expected: bool, ) -> None: from deepagents_code.auto_mode import AutoModeHITLMiddleware - if experimental: - monkeypatch.setenv(EXPERIMENTAL, "1") - else: - monkeypatch.delenv(EXPERIMENTAL, raising=False) + monkeypatch.delenv(EXPERIMENTAL, raising=False) mock_settings = self._build_mock_settings(tmp_path) mock_agent = Mock() @@ -4857,10 +4848,7 @@ def test_auto_mode_requires_experimental_flag( ) middleware = mock_create.call_args.kwargs["middleware"] - assert ( - any(isinstance(item, AutoModeHITLMiddleware) for item in middleware) - is expected - ) + assert any(isinstance(item, AutoModeHITLMiddleware) for item in middleware) assert "hitl_middleware" not in mock_create.call_args.kwargs def test_appends_rubric_middleware(self, tmp_path: Path) -> None: diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index f4c968e7258..b9cf7799407 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -24059,7 +24059,7 @@ async def aput_store_item( class TestLiveApprovalModeWrites: """Verify live approval-mode write and toggle failure behavior.""" - def test_auto_startup_requires_experimental_flag( + def test_auto_startup_does_not_require_experimental_flag( self, monkeypatch: pytest.MonkeyPatch ) -> None: from deepagents_code._env_vars import EXPERIMENTAL @@ -24069,19 +24069,17 @@ def test_auto_startup_requires_experimental_flag( app = DeepAgentsApp(approval_mode=ApprovalMode.AUTO) - assert app._approval_mode is ApprovalMode.MANUAL + assert app._approval_mode is ApprovalMode.AUTO - def test_auto_startup_enabled_by_experimental_flag( - self, monkeypatch: pytest.MonkeyPatch - ) -> None: - from deepagents_code._env_vars import EXPERIMENTAL + def test_auto_startup_unavailable_with_sandbox(self) -> None: from deepagents_code.approval_mode import ApprovalMode - monkeypatch.setenv(EXPERIMENTAL, "1") - - app = DeepAgentsApp(approval_mode=ApprovalMode.AUTO) + app = DeepAgentsApp( + approval_mode=ApprovalMode.AUTO, + server_kwargs={"sandbox_type": "daytona"}, + ) - assert app._approval_mode is ApprovalMode.AUTO + assert app._approval_mode is ApprovalMode.MANUAL async def test_write_live_approval_mode_records_key(self) -> None: from deepagents_code.approval_mode import ( diff --git a/libs/code/tests/unit_tests/test_main_args.py b/libs/code/tests/unit_tests/test_main_args.py index d2873864427..c921be550b8 100644 --- a/libs/code/tests/unit_tests/test_main_args.py +++ b/libs/code/tests/unit_tests/test_main_args.py @@ -341,7 +341,7 @@ def test_accepts_auto_approve_in_interactive_mode(self) -> None: with ( patch.object(sys, "argv", ["deepagents", "--auto-approve", "-m", "hello"]), patch.object(sys, "stdin", mock_stdin), - patch.dict(os.environ, {"DEEPAGENTS_CODE_EXPERIMENTAL": "1"}), + patch.dict(os.environ, {"DEEPAGENTS_CODE_EXPERIMENTAL": "0"}), patch("deepagents_code.main.run_textual_cli_async", run_tui), patch("deepagents_code.main._run_startup_auto_update"), patch("deepagents_code.main._resolve_agent_arg", return_value="agent"), diff --git a/libs/code/tests/unit_tests/test_server_graph.py b/libs/code/tests/unit_tests/test_server_graph.py index 761955dcb89..b57e0c1cfc4 100644 --- a/libs/code/tests/unit_tests/test_server_graph.py +++ b/libs/code/tests/unit_tests/test_server_graph.py @@ -270,7 +270,7 @@ async def cleanup(self) -> None: system_prompt=None, interactive=True, auto_approve=False, - auto_mode_enabled=False, + auto_mode_enabled=True, interrupt_shell_only=False, shell_allow_list=None, fs_tools=["ls", "read_file"], From 33884b53a351a033837d866a05a01540ee6259cc Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Thu, 23 Jul 2026 20:27:49 -0400 Subject: [PATCH 2/4] fix(code): drop undefined `expected` in auto-mode agent test --- libs/code/deepagents_code/agent.py | 1 - libs/code/tests/unit_tests/test_agent.py | 40 +++++++++++------------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/libs/code/deepagents_code/agent.py b/libs/code/deepagents_code/agent.py index de1108ef965..fdd1995993e 100644 --- a/libs/code/deepagents_code/agent.py +++ b/libs/code/deepagents_code/agent.py @@ -59,7 +59,6 @@ from deepagents_code import theme from deepagents_code._cli_context import CLIContextSchema from deepagents_code._constants import DEFAULT_AGENT_NAME -from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy from deepagents_code._glm_5p2_profile import ( _ensure_glm_5p2_profile_registered, _GlmTerminalStallRecovery, diff --git a/libs/code/tests/unit_tests/test_agent.py b/libs/code/tests/unit_tests/test_agent.py index bac3148f6aa..bfb95d8e5fe 100644 --- a/libs/code/tests/unit_tests/test_agent.py +++ b/libs/code/tests/unit_tests/test_agent.py @@ -4765,29 +4765,25 @@ def test_auto_mode_does_not_require_experimental_flag( middleware = mock_create.call_args.kwargs["middleware"] assert any(isinstance(item, AutoModeHITLMiddleware) for item in middleware) assert "hitl_middleware" not in mock_create.call_args.kwargs - if expected: - from deepagents_code.ask_user import AskUserMiddleware - from deepagents_code.offload_middleware import CLICompactionMiddleware + from deepagents_code.ask_user import AskUserMiddleware + from deepagents_code.offload_middleware import CLICompactionMiddleware - auto_middleware = next( - item for item in middleware if isinstance(item, AutoModeHITLMiddleware) - ) - ask_user_middleware = next( - item for item in middleware if isinstance(item, AskUserMiddleware) - ) - compaction_middleware = next( - item for item in middleware if isinstance(item, CLICompactionMiddleware) - ) - assert ( - auto_middleware._trusted_ask_user_tool is ask_user_middleware.tools[0] - ) - assert ( - auto_middleware._trusted_compaction_tool - is compaction_middleware.tools[0] - ) - assert middleware.index(auto_middleware) < middleware.index( - compaction_middleware - ) + auto_middleware = next( + item for item in middleware if isinstance(item, AutoModeHITLMiddleware) + ) + ask_user_middleware = next( + item for item in middleware if isinstance(item, AskUserMiddleware) + ) + compaction_middleware = next( + item for item in middleware if isinstance(item, CLICompactionMiddleware) + ) + assert auto_middleware._trusted_ask_user_tool is ask_user_middleware.tools[0] + assert ( + auto_middleware._trusted_compaction_tool is compaction_middleware.tools[0] + ) + assert middleware.index(auto_middleware) < middleware.index( + compaction_middleware + ) def test_compiled_agent_preserves_canonical_compaction_tool_identity( self, tmp_path: Path From c07da7fc7765e9fa87d12f4ffeedfd9765c1edb4 Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Thu, 23 Jul 2026 20:39:00 -0400 Subject: [PATCH 3/4] fix(code): drop Auto beta wording and residual experimental tests Remove beta framing from Auto mode help/copy now that the feature is GA, delete needless experimental-flag regression tests, and stop patching the experimental env in interactive --auto-approve coverage. --- libs/code/deepagents_code/_env_vars.py | 4 +- libs/code/deepagents_code/main.py | 2 +- libs/code/deepagents_code/ui.py | 4 +- libs/code/tests/unit_tests/test_agent.py | 69 +------------------- libs/code/tests/unit_tests/test_app.py | 12 ---- libs/code/tests/unit_tests/test_main_args.py | 1 - 6 files changed, 6 insertions(+), 86 deletions(-) diff --git a/libs/code/deepagents_code/_env_vars.py b/libs/code/deepagents_code/_env_vars.py index 237ab33431b..afaf0b3c316 100644 --- a/libs/code/deepagents_code/_env_vars.py +++ b/libs/code/deepagents_code/_env_vars.py @@ -159,8 +159,8 @@ """Opt into experimental, unstable dcode behavior. Off by default; parsed by `is_env_truthy` (see there for the accepted truthy -values). Currently gates the beta classifier-backed Auto approval mode. -Behavior behind this flag may change or be removed without notice. +values). Marks experimental runs in UI/trace metadata. Behavior behind this +flag may change or be removed without notice. """ EXTERNAL_EVENT_SOCKET = "DEEPAGENTS_CODE_EXTERNAL_EVENT_SOCKET" diff --git a/libs/code/deepagents_code/main.py b/libs/code/deepagents_code/main.py index 4ce000a40fe..05e5732cfb6 100644 --- a/libs/code/deepagents_code/main.py +++ b/libs/code/deepagents_code/main.py @@ -2012,7 +2012,7 @@ def help_parent(help_fn: Callable[[], None]) -> list[argparse.ArgumentParser]: "--auto-approve", action="store_true", default=None, - help="Interactive local TUI only: enable beta classifier-backed Auto mode.", + help="Interactive local TUI only: enable classifier-backed Auto mode.", ) approval_group.add_argument( "--yolo", diff --git a/libs/code/deepagents_code/ui.py b/libs/code/deepagents_code/ui.py index 2a312b05eea..4ff1c37b24d 100644 --- a/libs/code/deepagents_code/ui.py +++ b/libs/code/deepagents_code/ui.py @@ -142,9 +142,7 @@ def show_help() -> None: console.print( " --startup-cmd CMD Shell command to run at startup, before first prompt" # noqa: E501 ) - console.print( - " -y, --auto-approve Enable beta classifier-backed Auto mode" - ) + console.print(" -y, --auto-approve Enable classifier-backed Auto mode") console.print( " --yolo Run gated actions without review after " "acknowledgement" diff --git a/libs/code/tests/unit_tests/test_agent.py b/libs/code/tests/unit_tests/test_agent.py index bfb95d8e5fe..906b295fcaf 100644 --- a/libs/code/tests/unit_tests/test_agent.py +++ b/libs/code/tests/unit_tests/test_agent.py @@ -24,7 +24,6 @@ from langgraph.runtime import Runtime from deepagents_code._cli_context import CLIContext, CLIContextSchema -from deepagents_code._env_vars import EXPERIMENTAL from deepagents_code._repository_bounds import REPOSITORY_TOOL_CALL_LIMIT from deepagents_code.agent import ( _MEMORY_READONLY_SYSTEM_PROMPT, @@ -4423,9 +4422,8 @@ class TestAutoModeSubagentHITLWiring: """Auto-mode async HITL reaches every dcode subagent stack. These tests capture the `create_deep_agent` kwargs and assert that, in Auto - mode (gated behind `DEEPAGENTS_CODE_EXPERIMENTAL`), the async approval - middleware reaches both custom subagents and the general-purpose subagent - that dcode auto-adds. + mode, the async approval middleware reaches both custom subagents and the + general-purpose subagent that dcode auto-adds. """ @staticmethod @@ -4512,10 +4510,8 @@ def _capture_create_deep_agent_kwargs( async def test_async_hitl_covers_declarative_and_general_subagents_in_auto( self, tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, ) -> None: """Both CLI subagent forms bypass stock HITL from the async Store.""" - monkeypatch.setenv(EXPERIMENTAL, "1") kwargs = self._capture_create_deep_agent_kwargs( tmp_path, auto_mode_enabled=True, @@ -4543,10 +4539,8 @@ async def test_async_hitl_covers_declarative_and_general_subagents_in_auto( async def test_async_hitl_covers_declarative_and_general_subagents_in_manual( self, tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, ) -> None: """Both CLI subagent forms retain their stock Manual interrupt.""" - monkeypatch.setenv(EXPERIMENTAL, "1") kwargs = self._capture_create_deep_agent_kwargs( tmp_path, auto_mode_enabled=True, @@ -4726,65 +4720,6 @@ def _build_mock_settings(tmp_path: Path) -> Mock: mock_settings.interpreter_ptc_acknowledge_unsafe = False return mock_settings - def test_auto_mode_does_not_require_experimental_flag( - self, - tmp_path: Path, - monkeypatch: pytest.MonkeyPatch, - ) -> None: - from deepagents_code.auto_mode import AutoModeHITLMiddleware - - monkeypatch.delenv(EXPERIMENTAL, raising=False) - - mock_settings = self._build_mock_settings(tmp_path) - mock_agent = Mock() - mock_agent.with_config.return_value = mock_agent - fake_model = _make_fake_chat_model() - with ( - patch("deepagents_code.agent.settings", mock_settings), - patch("deepagents_code.agent.PluginSkillsMiddleware"), - patch("deepagents_code.agent.MemoryMiddleware"), - patch( - "deepagents_code.agent.create_deep_agent", - return_value=mock_agent, - ) as mock_create, - patch( - "deepagents._models.init_chat_model", - return_value=fake_model, - ), - ): - create_cli_agent( - model="fake-model", - assistant_id="test", - enable_memory=False, - enable_skills=False, - enable_shell=False, - auto_mode_enabled=True, - cwd=tmp_path, - ) - - middleware = mock_create.call_args.kwargs["middleware"] - assert any(isinstance(item, AutoModeHITLMiddleware) for item in middleware) - assert "hitl_middleware" not in mock_create.call_args.kwargs - from deepagents_code.ask_user import AskUserMiddleware - from deepagents_code.offload_middleware import CLICompactionMiddleware - - auto_middleware = next( - item for item in middleware if isinstance(item, AutoModeHITLMiddleware) - ) - ask_user_middleware = next( - item for item in middleware if isinstance(item, AskUserMiddleware) - ) - compaction_middleware = next( - item for item in middleware if isinstance(item, CLICompactionMiddleware) - ) - assert auto_middleware._trusted_ask_user_tool is ask_user_middleware.tools[0] - assert ( - auto_middleware._trusted_compaction_tool is compaction_middleware.tools[0] - ) - assert middleware.index(auto_middleware) < middleware.index( - compaction_middleware - ) - def test_compiled_agent_preserves_canonical_compaction_tool_identity( self, tmp_path: Path ) -> None: diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index 92a33f2d7a1..3fa437969b3 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -25271,18 +25271,6 @@ async def aput_store_item( class TestLiveApprovalModeWrites: """Verify live approval-mode write and toggle failure behavior.""" - def test_auto_startup_does_not_require_experimental_flag( - self, monkeypatch: pytest.MonkeyPatch - ) -> None: - from deepagents_code._env_vars import EXPERIMENTAL - from deepagents_code.approval_mode import ApprovalMode - - monkeypatch.delenv(EXPERIMENTAL, raising=False) - - app = DeepAgentsApp(approval_mode=ApprovalMode.AUTO) - - assert app._approval_mode is ApprovalMode.AUTO - def test_auto_startup_unavailable_with_sandbox(self) -> None: from deepagents_code.approval_mode import ApprovalMode diff --git a/libs/code/tests/unit_tests/test_main_args.py b/libs/code/tests/unit_tests/test_main_args.py index c921be550b8..fcd61760e3c 100644 --- a/libs/code/tests/unit_tests/test_main_args.py +++ b/libs/code/tests/unit_tests/test_main_args.py @@ -341,7 +341,6 @@ def test_accepts_auto_approve_in_interactive_mode(self) -> None: with ( patch.object(sys, "argv", ["deepagents", "--auto-approve", "-m", "hello"]), patch.object(sys, "stdin", mock_stdin), - patch.dict(os.environ, {"DEEPAGENTS_CODE_EXPERIMENTAL": "0"}), patch("deepagents_code.main.run_textual_cli_async", run_tui), patch("deepagents_code.main._run_startup_auto_update"), patch("deepagents_code.main._resolve_agent_arg", return_value="agent"), From 9be9bc5365c776ece06bd4d5314d0f21b3d4a94d Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Thu, 23 Jul 2026 21:15:57 -0400 Subject: [PATCH 4/4] fix(code): correct Auto sandbox messaging and add GA guard tests Auto is now generally available, so the sandbox-unavailable toast no longer misattributes the block to "the local TUI", and residual "beta" labels are dropped from the enabled warnings. Adds coverage for the branches the GA change restructured: Auto wires its middleware when interactive and sandbox-free, is refused with a sandbox or when non-interactive, resolves at startup with EXPERIMENTAL explicitly off, and downgrades to Manual (with a reason) under --sandbox. --- libs/code/deepagents_code/app.py | 8 +- libs/code/tests/unit_tests/test_agent.py | 98 ++++++++++++++++++++ libs/code/tests/unit_tests/test_app.py | 18 ++++ libs/code/tests/unit_tests/test_main_args.py | 59 ++++++++++++ 4 files changed, 179 insertions(+), 4 deletions(-) diff --git a/libs/code/deepagents_code/app.py b/libs/code/deepagents_code/app.py index 4baa269450c..34e3fd92ddd 100644 --- a/libs/code/deepagents_code/app.py +++ b/libs/code/deepagents_code/app.py @@ -130,7 +130,7 @@ ) _AUTO_MODE_ENABLED_WARNING = ( - "Auto beta enabled. It classifies gated actions but is not sandbox containment." + "Auto enabled. It classifies gated actions but is not sandbox containment." ) @@ -7521,7 +7521,7 @@ async def _on_auto_approve_enabled(self) -> bool: if not self._auto_mode_eligible: self._warn_live_approval_mode_unavailable( - "Auto is available only in the local TUI." + "Auto is unavailable with a sandbox." ) return False if not await self._write_live_approval_mode(ApprovalMode.AUTO): @@ -16815,7 +16815,7 @@ async def action_toggle_auto_approve(self) -> None: if self._approval_mode is ApprovalMode.MANUAL: if not self._auto_mode_eligible: self._warn_live_approval_mode_unavailable( - "Auto is available only in the local TUI." + "Auto is unavailable with a sandbox." ) return target = ApprovalMode.AUTO @@ -16853,7 +16853,7 @@ async def action_toggle_auto_approve(self) -> None: self._status_bar.set_approval_mode(target.value) if target is ApprovalMode.AUTO: self.notify( - "Automated review (beta) is enabled. It checks approval-gated " + "Automated review is enabled. It checks approval-gated " "actions, but may not catch every issue.", severity="warning", timeout=8, diff --git a/libs/code/tests/unit_tests/test_agent.py b/libs/code/tests/unit_tests/test_agent.py index 906b295fcaf..018fa5cbd49 100644 --- a/libs/code/tests/unit_tests/test_agent.py +++ b/libs/code/tests/unit_tests/test_agent.py @@ -4720,6 +4720,104 @@ def _build_mock_settings(tmp_path: Path) -> Mock: mock_settings.interpreter_ptc_acknowledge_unsafe = False return mock_settings + def _capture_middleware(self, tmp_path: Path, **kwargs: Any) -> list[Any]: + """Run `create_cli_agent` with mocked deps and return its middleware list. + + Keeps the Auto-mode wiring tests below to a single assertion apiece by + centralizing the identical patching/boilerplate. Extra keyword + arguments (e.g. `auto_mode_enabled`, `interactive`, `sandbox`) are + forwarded to `create_cli_agent`. + """ + mock_settings = self._build_mock_settings(tmp_path) + mock_agent = Mock() + mock_agent.with_config.return_value = mock_agent + fake_model = _make_fake_chat_model() + with ( + patch("deepagents_code.agent.settings", mock_settings), + patch("deepagents_code.agent.PluginSkillsMiddleware"), + patch("deepagents_code.agent.MemoryMiddleware"), + patch( + "deepagents_code.agent.create_deep_agent", + return_value=mock_agent, + ) as mock_create, + patch( + "deepagents._models.init_chat_model", + return_value=fake_model, + ), + ): + create_cli_agent( + model="fake-model", + assistant_id="test", + enable_memory=False, + enable_skills=False, + enable_shell=False, + cwd=tmp_path, + **kwargs, + ) + return mock_create.call_args.kwargs["middleware"] + + def test_auto_mode_enabled_wires_middleware(self, tmp_path: Path) -> None: + """Auto wires `AutoModeHITLMiddleware` in the interactive, sandbox-free case. + + Regression guard for GA: Auto no longer requires an experimental flag, + so an interactive local session with `auto_mode_enabled=True` must + install the middleware and bind the canonical ask-user/compaction tools, + ordered ahead of compaction. + """ + from deepagents_code.ask_user import AskUserMiddleware + from deepagents_code.auto_mode import AutoModeHITLMiddleware + from deepagents_code.offload_middleware import CLICompactionMiddleware + + middleware = self._capture_middleware(tmp_path, auto_mode_enabled=True) + + auto_middleware = next( + item for item in middleware if isinstance(item, AutoModeHITLMiddleware) + ) + ask_user_middleware = next( + item for item in middleware if isinstance(item, AskUserMiddleware) + ) + compaction_middleware = next( + item for item in middleware if isinstance(item, CLICompactionMiddleware) + ) + assert auto_middleware._trusted_ask_user_tool is ask_user_middleware.tools[0] + assert ( + auto_middleware._trusted_compaction_tool is compaction_middleware.tools[0] + ) + assert middleware.index(auto_middleware) < middleware.index( + compaction_middleware + ) + + def test_auto_mode_omitted_outside_interactive(self, tmp_path: Path) -> None: + """Auto is refused (no middleware) in a non-interactive session.""" + from deepagents_code.auto_mode import AutoModeHITLMiddleware + + middleware = self._capture_middleware( + tmp_path, auto_mode_enabled=True, interactive=False + ) + + assert not any(isinstance(item, AutoModeHITLMiddleware) for item in middleware) + + def test_auto_mode_omitted_with_sandbox(self, tmp_path: Path) -> None: + """Auto is refused (no middleware) when a sandbox backend is active. + + This guard is the sole programmatic protection preventing + classifier-backed auto-approval from engaging in a sandboxed session, + so it is asserted directly rather than relying on upstream callers. + """ + from deepagents.backends.filesystem import FilesystemBackend + + from deepagents_code.auto_mode import AutoModeHITLMiddleware + + sandbox = cast( + "SandboxBackendProtocol", + FilesystemBackend(root_dir=tmp_path, virtual_mode=False), + ) + middleware = self._capture_middleware( + tmp_path, auto_mode_enabled=True, sandbox=sandbox + ) + + assert not any(isinstance(item, AutoModeHITLMiddleware) for item in middleware) + def test_compiled_agent_preserves_canonical_compaction_tool_identity( self, tmp_path: Path ) -> None: diff --git a/libs/code/tests/unit_tests/test_app.py b/libs/code/tests/unit_tests/test_app.py index 7f41c7739dc..c8a14acbbc1 100644 --- a/libs/code/tests/unit_tests/test_app.py +++ b/libs/code/tests/unit_tests/test_app.py @@ -25372,6 +25372,24 @@ async def aput_store_item( class TestLiveApprovalModeWrites: """Verify live approval-mode write and toggle failure behavior.""" + def test_auto_startup_enabled_without_sandbox( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Auto resolves at startup without a sandbox, even with EXPERIMENTAL off. + + Auto is now generally available; the former experimental opt-in is gone. + Setting `DEEPAGENTS_CODE_EXPERIMENTAL` explicitly falsy proves the flag + no longer gates Auto, so this stays load-bearing rather than inert. + """ + from deepagents_code._env_vars import EXPERIMENTAL + from deepagents_code.approval_mode import ApprovalMode + + monkeypatch.setenv(EXPERIMENTAL, "0") + + app = DeepAgentsApp(approval_mode=ApprovalMode.AUTO) + + assert app._approval_mode is ApprovalMode.AUTO + def test_auto_startup_unavailable_with_sandbox(self) -> None: from deepagents_code.approval_mode import ApprovalMode diff --git a/libs/code/tests/unit_tests/test_main_args.py b/libs/code/tests/unit_tests/test_main_args.py index fcd61760e3c..ea21cebf9c9 100644 --- a/libs/code/tests/unit_tests/test_main_args.py +++ b/libs/code/tests/unit_tests/test_main_args.py @@ -364,6 +364,65 @@ def test_accepts_auto_approve_in_interactive_mode(self) -> None: assert await_args.kwargs["approval_mode"] is ApprovalMode.AUTO + def test_auto_approve_downgraded_to_manual_with_sandbox( + self, capsys: pytest.CaptureFixture[str] + ) -> None: + """`--auto-approve` with a sandbox must downgrade to Manual and warn. + + Auto's classifier runs only in the sandbox-free local TUI. When a + sandbox is requested the interactive launch path (`cli_main`) must + resolve Manual and surface the reason instead of silently dropping the + requested mode. + """ + from deepagents_code.main import cli_main + + mock_stdin = MagicMock() + mock_stdin.isatty.return_value = True + + fake_result = MagicMock() + fake_result.return_code = 0 + fake_result.thread_id = None + fake_result.update_available = (False, None) + fake_result.session_stats = MagicMock(request_count=0) + run_tui = AsyncMock(return_value=fake_result) + + with ( + patch.object( + sys, + "argv", + ["deepagents", "--auto-approve", "--sandbox", "daytona", "-m", "hi"], + ), + patch.object(sys, "stdin", mock_stdin), + # Skip the real provider dependency check; it exits before the + # approval-mode downgrade when `daytona` extras are absent. + patch( + "deepagents_code.integrations.sandbox_factory.verify_sandbox_deps", + return_value=None, + ), + patch("deepagents_code.main.run_textual_cli_async", run_tui), + patch("deepagents_code.main._run_startup_auto_update"), + patch("deepagents_code.main._resolve_agent_arg", return_value="agent"), + patch("deepagents_code.main._check_mcp_project_trust", return_value=False), + patch( + "deepagents_code.main._resolve_interpreter_enabled", + return_value=False, + ), + patch("deepagents_code.main._print_session_stats"), + patch( + "deepagents_code.main._should_check_teardown_thread", + return_value=False, + ), + ): + cli_main() + + run_tui.assert_awaited_once() + await_args = run_tui.await_args + assert await_args is not None + from deepagents_code.approval_mode import ApprovalMode + + assert await_args.kwargs["approval_mode"] is ApprovalMode.MANUAL + assert "Auto is unavailable with a sandbox" in capsys.readouterr().out + @pytest.mark.parametrize( ("input_str", "expected"),