Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions libs/code/deepagents_code/_env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 0 additions & 7 deletions libs/code/deepagents_code/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -2325,12 +2324,6 @@ def create_cli_agent(
"""
tools = tools or []
mcp_tools = tuple(mcp_tools or ())
if auto_mode_enabled and not is_env_truthy(EXPERIMENTAL):
Comment thread
open-swe[bot] marked this conversation as resolved.
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 "
Expand Down
14 changes: 5 additions & 9 deletions libs/code/deepagents_code/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)


Expand Down Expand Up @@ -3103,11 +3103,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
Expand Down Expand Up @@ -7525,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 opt-in local TUI beta."
"Auto is unavailable with a sandbox."
)
return False
if not await self._write_live_approval_mode(ApprovalMode.AUTO):
Expand Down Expand Up @@ -16819,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 opt-in local TUI beta."
"Auto is unavailable with a sandbox."
)
return
target = ApprovalMode.AUTO
Expand Down Expand Up @@ -16857,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,
Expand Down
21 changes: 8 additions & 13 deletions libs/code/deepagents_code/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2012,10 +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. "
"Requires DEEPAGENTS_CODE_EXPERIMENTAL=1."
),
help="Interactive local TUI only: enable classifier-backed Auto mode.",
)
approval_group.add_argument(
"--yolo",
Expand Down Expand Up @@ -4666,20 +4663,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
Expand Down
8 changes: 1 addition & 7 deletions libs/code/deepagents_code/server_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,13 +280,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.
Expand Down
9 changes: 4 additions & 5 deletions libs/code/deepagents_code/tui/widgets/approval.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging Mason Daugherty (@mdrxy) - why would this be false when a sandbox is active? Surely there are actions we potentially want to deny even when in a sandbox?

Another question would be - do we really need this auto_mode_eligible attr once we're in GA?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok looks like this is a deliberate decision - would you provide some info so I understand?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto is local-only for now on purpose.

Under the hood Auto does more than “skip the approve prompt.” It makes judgment calls using context about this machine’s project: where the repo root is, what the git remote looks like, and whether a path is inside that trusted tree. Those checks are built for a normal local worktree on the host.

A sandbox breaks that assumption. Tools aren’t writing to the same FS the host process is looking at — paths, symlinks, and “where is the project root?” can mean something different inside Daytona/Runloop/etc. If we naively reused the host-side checks, Auto could allow or deny for the wrong reasons. Same issue for the temporary files Auto has the agent use for scratch: those are allocated in the host temp dir today, which isn’t the same place sandbox tools run.

Important distinction: sandbox sessions still get normal Manual approvals in the TUI. We’re only holding back the classifier auto-approve path until it’s honest there.

Making Auto work with sandboxes later isn’t a new product mode. Same Shift+Tab / fallback UI. We’d just need the policy to reason about the sandbox's workspace (remote root, remote path semantics, scratch that actually lives where tools run — or host-temp disabled under sandbox). Once that binding is solid, Auto + sandbox can ship together.

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)
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 1 addition & 3 deletions libs/code/deepagents_code/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
121 changes: 69 additions & 52 deletions libs/code/tests/unit_tests/test_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -4726,24 +4720,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(
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)
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
Expand All @@ -4767,39 +4751,72 @@ def test_auto_mode_requires_experimental_flag(
enable_memory=False,
enable_skills=False,
enable_shell=False,
auto_mode_enabled=True,
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.

middleware = mock_create.call_args.kwargs["middleware"]
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 (
any(isinstance(item, AutoModeHITLMiddleware) for item in middleware)
is expected
auto_middleware._trusted_compaction_tool is compaction_middleware.tools[0]
)
assert middleware.index(auto_middleware) < middleware.index(
compaction_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

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
Expand Down
26 changes: 15 additions & 11 deletions libs/code/tests/unit_tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -25372,29 +25372,33 @@ 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_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.delenv(EXPERIMENTAL, raising=False)
monkeypatch.setenv(EXPERIMENTAL, "0")

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 (
Expand Down
Loading