feat(code): enable js_eval by default (#4245)

Local dcode sessions now start with the JS interpreter and safe PTC
bridge available by default, while remote sandbox launches keep it
disabled unless explicitly requested. `langchain-quickjs` is now a core
dependency, so users no longer need to install the `quickjs` extra just
to use interpreter support.
This commit is contained in:
Mason Daugherty
2026-06-25 04:26:41 -04:00
committed by GitHub
parent 4432e0810e
commit 2e04ff397e
22 changed files with 814 additions and 116 deletions
+75 -3
View File
@@ -101,6 +101,67 @@ def _read_env_optional_bool(suffix: str) -> bool | None:
return raw.lower() == "true"
def _resolve_enable_interpreter(
enable_interpreter: bool | None, sandbox_type: str | None
) -> bool:
"""Resolve the interpreter's tri-state caller option to a concrete boolean.
Args:
enable_interpreter: Explicit caller preference, or `None` to use the
sandbox-aware default.
sandbox_type: Sandbox backend identifier. Any falsy value (`None`, `""`)
or `"none"` is treated as local execution.
Returns:
The explicit `enable_interpreter` value when not `None`; `False` for
remote-sandbox defaults; otherwise the configured local default
(`settings.enable_interpreter`).
"""
if enable_interpreter is not None:
return enable_interpreter
if sandbox_type and sandbox_type != "none":
return False
from deepagents_code.config import settings
return settings.enable_interpreter
def _interpreter_suppressed_by_sandbox(
*, enable_interpreter: bool | None, sandbox_type: str | None, local_default: bool
) -> bool:
"""Whether a remote sandbox suppressed the otherwise-default interpreter.
Used to decide whether to surface an advisory: returns `True` only when the
user made no explicit choice, a remote sandbox is active, and the local
default would have enabled it — i.e. the sandbox (not an explicit
`--no-interpreter` opt-out, nor a disabled `[interpreter]` config) is why
`js_eval` is unavailable.
Takes the *raw* tri-state caller intent rather than the resolved boolean: a
sandbox-suppressed default and an explicit `--no-interpreter` both resolve to
`False`, so the resolved value cannot distinguish them. Any explicit choice
(`not None`) is the user's own decision and is left unannounced.
Args:
enable_interpreter: The raw tri-state caller intent (`--interpreter` →
`True`, `--no-interpreter` → `False`, unset → `None`).
sandbox_type: Sandbox backend identifier. Any falsy value (`None`, `""`)
or `"none"` is treated as local execution.
local_default: The local-mode default (`settings.enable_interpreter`);
gating on it keeps the advisory quiet for users who disabled the
interpreter in config.
Returns:
`True` when the advisory should be shown, otherwise `False`.
"""
if enable_interpreter is not None:
return False
if not (sandbox_type and sandbox_type != "none"):
return False
return local_default
@dataclass(frozen=True)
class ServerConfig:
"""Full configuration payload passed from the app to the server subprocess.
@@ -155,6 +216,13 @@ class ServerConfig:
enable_interpreter: bool = False
"""Enable `CodeInterpreterMiddleware` (`js_eval` REPL) on the main agent.
Always the resolved concrete value: `from_cli_args` collapses the tri-state
caller option via `_resolve_enable_interpreter` before constructing the
config, so the `bool | None` "defer to default" sentinel never reaches this
field. The `False` default here is only the bare-constructor/`from_env`
fallback; the user-facing default (on in local mode) lives in
`settings.enable_interpreter`.
Local-mode only; the server graph raises if a sandbox is configured and
this flag is `True`.
"""
@@ -341,7 +409,7 @@ class ServerConfig:
sandbox_setup: str | None,
enable_shell: bool,
enable_ask_user: bool,
enable_interpreter: bool = False,
enable_interpreter: bool | None = None,
interpreter_ptc: str | list[str] | None = None,
interpreter_ptc_acknowledge_unsafe: bool = False,
mcp_config_path: str | None,
@@ -373,7 +441,7 @@ class ServerConfig:
enable_shell: Enable shell execution tools.
enable_ask_user: Enable ask_user tool.
enable_interpreter: Enable `CodeInterpreterMiddleware` on the main
agent.
agent. `None` uses the sandbox-aware default.
interpreter_ptc: Override for `settings.interpreter_ptc`.
interpreter_ptc_acknowledge_unsafe: Mirror of
`settings.interpreter_ptc_acknowledge_unsafe`.
@@ -387,6 +455,10 @@ class ServerConfig:
"""
normalized_mcp = _normalize_path(mcp_config_path, project_context, "MCP config")
resolved_enable_interpreter = _resolve_enable_interpreter(
enable_interpreter, sandbox_type
)
return cls(
model=model_name,
model_params=model_params,
@@ -397,7 +469,7 @@ class ServerConfig:
interactive=interactive,
enable_shell=enable_shell,
enable_ask_user=enable_ask_user,
enable_interpreter=enable_interpreter,
enable_interpreter=resolved_enable_interpreter,
interpreter_ptc=interpreter_ptc,
interpreter_ptc_acknowledge_unsafe=interpreter_ptc_acknowledge_unsafe,
sandbox_type=sandbox_type,
+1 -2
View File
@@ -1306,8 +1306,7 @@ def create_cli_agent(
list or `interpreter_ptc="all"` with
`interpreter_ptc_acknowledge_unsafe=True`.
Requires the `quickjs` optional extra
(`langchain-quickjs>=0.3.1,<0.4.0`).
Requires the core `langchain-quickjs` dependency.
checkpointer: Optional checkpointer for session persistence.
When `None`, the graph is compiled without a checkpointer.
mcp_server_info: MCP server metadata to surface in the system prompt.
+52 -20
View File
@@ -1582,6 +1582,7 @@ class DeepAgentsApp(App):
mcp_preload_kwargs: dict[str, Any] | None = None,
model_kwargs: dict[str, Any] | None = None,
model_explicitly_set: bool = False,
interpreter_arg: bool | None = None,
defer_server_start: bool = False,
title: str | None = None,
sub_title: str | None = None,
@@ -1640,6 +1641,11 @@ class DeepAgentsApp(App):
When `True`, an explicit choice wins over the model persisted
in a resumed thread (no resume adoption).
interpreter_arg: The raw `--interpreter`/`--no-interpreter` tri-state
(`True`/`False`/`None`). Used only to distinguish an explicit
opt-out from a sandbox-suppressed default when surfacing the
disabled-by-sandbox advisory; the resolved value travels in
`server_kwargs`.
defer_server_start: Whether to keep app-owned server startup paused
until the user configures credentials or explicitly picks a model.
title: Override the Textual `App.title` shown in the optional
@@ -1880,6 +1886,8 @@ class DeepAgentsApp(App):
"""
self._model_explicitly_set = model_explicitly_set
self._interpreter_arg = interpreter_arg
"""Raw `--interpreter`/`--no-interpreter` tri-state for advisory gating."""
"""Whether `--model` was passed on the command line.
Suppresses adopting a resumed thread's persisted model.
@@ -2471,6 +2479,7 @@ class DeepAgentsApp(App):
# Non-essential advisory: defer past first paint so it never delays
# the initial frame.
self.call_after_refresh(self._notify_interpreter_tools_without_interpreter)
self.call_after_refresh(self._notify_interpreter_disabled_by_sandbox)
self.call_after_refresh(self._notify_orphaned_tracing_disabled)
def _notify_orphaned_tracing_disabled(self) -> None:
@@ -2491,7 +2500,7 @@ class DeepAgentsApp(App):
logger.exception("Failed to surface orphaned-tracing disabled notice")
def _notify_interpreter_tools_without_interpreter(self) -> None:
"""Toast when `--interpreter-tools` was set without `--interpreter`.
"""Toast when `--interpreter-tools` was set while the interpreter is off.
The PTC allowlist applies only when the interpreter middleware is
enabled, so the flag is a no-op on its own. This is the TUI counterpart
@@ -2509,7 +2518,37 @@ class DeepAgentsApp(App):
if server_kwargs.get("enable_interpreter"):
return
self.notify(
"--interpreter-tools has no effect unless --interpreter is set.",
"--interpreter-tools has no effect when the interpreter is disabled.",
severity="warning",
markup=False,
)
def _notify_interpreter_disabled_by_sandbox(self) -> None:
"""Toast when a remote sandbox suppressed the otherwise-default interpreter.
`js_eval` is on by default in local mode but unsupported under a remote
sandbox, so a `--sandbox` run silently drops it. A stderr line would be
clobbered by the alternate screen, so the advisory is surfaced here as a
startup notification the TUI counterpart of the non-interactive warning
in `main._warn_if_interpreter_disabled_by_sandbox`.
Gated on the raw `--interpreter` tri-state (`self._interpreter_arg`) so an
explicit `--no-interpreter` opt-out stays quiet, and on the local default
from `settings` so users who disabled the interpreter in config are not
nagged.
"""
from deepagents_code._server_config import _interpreter_suppressed_by_sandbox
from deepagents_code.config import settings
if not _interpreter_suppressed_by_sandbox(
enable_interpreter=self._interpreter_arg,
sandbox_type=self._sandbox_type,
local_default=settings.enable_interpreter,
):
return
self.notify(
"JS interpreter (js_eval) is unavailable under a remote sandbox; "
"it runs in local mode only.",
severity="warning",
markup=False,
)
@@ -4365,7 +4404,7 @@ class DeepAgentsApp(App):
async def _handle_install_command(self, command: str) -> None:
"""Handle the `/install <extra>` slash command.
Adds an optional extra (e.g. `quickjs`, `daytona`) to the installed
Adds an optional extra (e.g. `daytona`, `fireworks`) to the installed
dcode tool by re-running
`uv tool install --reinstall -U 'deepagents-code[<extra>]'`.
Refuses unknown extras unless the user passes a `--force` token.
@@ -4388,7 +4427,7 @@ class DeepAgentsApp(App):
AppMessage(
"Usage: /install <extra> [--force]\n"
" /install <package> --package [--force]\n"
"Example: /install quickjs\n\n"
"Example: /install daytona\n\n"
f"{format_known_extras()}",
),
)
@@ -4421,7 +4460,7 @@ class DeepAgentsApp(App):
a one-keypress restart for restart-capable extras.
Args:
extra: The extra name to install (e.g. `"baseten"`, `"quickjs"`).
extra: The extra name to install (e.g. `"baseten"`, `"daytona"`).
force: Skip the "unknown extra" guard for valid-but-unlisted names.
auto_restart: Restart the app-owned server immediately after a
restart-capable install. Used only when the user selected a model
@@ -4585,10 +4624,8 @@ class DeepAgentsApp(App):
# Model-provider and sandbox extras are imported by the langgraph
# server subprocess; `/restart` respawns that subprocess and picks
# them up without exiting the TUI. `quickjs` (and other
# STANDALONE_EXTRAS) are wired into the parent process at startup
# (`verify_interpreter_deps` gates `--interpreter`), so a full
# relaunch is required.
# them up without exiting the TUI. STANDALONE_EXTRAS are wired into
# the parent process at startup, so a full relaunch is required.
restart_capable = extra in MODEL_PROVIDER_EXTRAS or extra in SANDBOX_EXTRAS
if restart_capable and auto_restart:
if self._restart_after_install_is_unneeded():
@@ -4620,17 +4657,7 @@ class DeepAgentsApp(App):
return False
if not restart_capable:
if extra == "quickjs":
# `quickjs` only does anything behind `--interpreter`, a
# launch-only flag with a startup-time dependency gate, so a
# `/restart` (which reuses the original launch settings) can't
# enable it — a full relaunch with the flag is required.
next_step = (
"Exit and relaunch dcode with `--interpreter` to use it — "
"see `dcode --help`."
)
else:
next_step = "Exit and relaunch dcode to use the new dependencies."
next_step = "Exit and relaunch dcode to use the new dependencies."
await self._mount_message(
AppMessage(f"Installed extra '{extra}'. {next_step}"),
)
@@ -13598,6 +13625,7 @@ async def run_textual_app(
mcp_preload_kwargs: dict[str, Any] | None = None,
model_kwargs: dict[str, Any] | None = None,
model_explicitly_set: bool = False,
interpreter_arg: bool | None = None,
defer_server_start: bool = False,
title: str | None = None,
sub_title: str | None = None,
@@ -13649,6 +13677,9 @@ async def run_textual_app(
When `True`, the explicit choice wins over a resumed thread's
persisted model (no resume adoption).
interpreter_arg: The raw `--interpreter`/`--no-interpreter` tri-state,
forwarded to the app so the disabled-by-sandbox advisory can tell an
explicit opt-out from a sandbox-suppressed default.
defer_server_start: Whether to keep app-owned server startup paused
until credentials or a model are configured from inside the TUI.
title: Override the Textual `App.title` shown in the optional header
@@ -13679,6 +13710,7 @@ async def run_textual_app(
mcp_preload_kwargs=mcp_preload_kwargs,
model_kwargs=model_kwargs,
model_explicitly_set=model_explicitly_set,
interpreter_arg=interpreter_arg,
defer_server_start=defer_server_start,
title=title,
sub_title=sub_title,
+2 -2
View File
@@ -1817,7 +1817,7 @@ class Settings:
agent. Local-mode only; raises `ValueError` at agent-build time when a
remote sandbox is active. Subagents never receive the interpreter in v1.
The `quickjs` optional extra must be installed when this flag is `True`.
`langchain-quickjs` is installed as a core dependency.
Defaults are owned by `config_manifest` (the canonical config surface) so
they are defined in exactly one place.
@@ -1846,7 +1846,7 @@ class Settings:
Accepted values:
- `False` or `[]`: pure REPL, no `tools.*` bridge.
- `"safe"`: expand to `INTERPRETER_PTC_SAFE_PRESET`.
- `"safe"`: expand to `INTERPRETER_PTC_SAFE_PRESET` (the default).
- `"all"`: every tool passed to `create_cli_agent` is exposed. Requires
`interpreter_ptc_acknowledge_unsafe=True` when `auto_approve` is `False`.
- `list[str]`: explicit tool names. The list may also include the `"safe"`
+3 -3
View File
@@ -50,12 +50,12 @@ logger = logging.getLogger(__name__)
# These are the single source of truth for `[interpreter]` defaults. The
# `Settings` dataclass references them so the default is defined once.
INTERPRETER_ENABLE_DEFAULT = False
INTERPRETER_ENABLE_DEFAULT = True
INTERPRETER_TIMEOUT_SECONDS_DEFAULT = 5.0
INTERPRETER_MEMORY_LIMIT_MB_DEFAULT = 64
INTERPRETER_MAX_PTC_CALLS_DEFAULT = 256
INTERPRETER_MAX_RESULT_CHARS_DEFAULT = 4000
INTERPRETER_PTC_DEFAULT: str | bool | list[str] = False
INTERPRETER_PTC_DEFAULT: str | bool | list[str] = "safe"
INTERPRETER_PTC_ACKNOWLEDGE_UNSAFE_DEFAULT = False
LANGSMITH_PROJECT_DEFAULT = "deepagents-code"
@@ -949,7 +949,7 @@ _STATIC_OPTIONS: tuple[ConfigOption, ...] = (
kind=OptionKind.BOOL,
default=INTERPRETER_ENABLE_DEFAULT,
toml_keys=("interpreter", "enable_interpreter"),
cli_flag="--enable-interpreter",
cli_flag="--interpreter",
settings_field="enable_interpreter",
),
ConfigOption(
+11 -12
View File
@@ -106,11 +106,10 @@ SANDBOX_EXTRAS: frozenset[str] = frozenset(
"""Optional extras that add sandbox integrations."""
STANDALONE_EXTRAS: frozenset[str] = frozenset({"quickjs"})
"""Optional extras that don't fit the provider/sandbox taxonomy.
"""Compatibility extras that don't fit the provider/sandbox taxonomy.
These integrations layer onto the main agent (e.g. a JS REPL via
`langchain-quickjs`) and aren't grouped under `all-providers` or
`all-sandboxes`.
`quickjs` is a core dependency now, but the empty extra remains installable so
older `deepagents-code[quickjs]` and `/install quickjs` workflows stay harmless.
"""
KNOWN_EXTRAS: frozenset[str] = (
@@ -374,12 +373,12 @@ def extra_for_package(
def verify_interpreter_deps() -> None:
"""Check that `langchain-quickjs` is installed for the `--interpreter` flag.
"""Check that `langchain-quickjs` is installed for the interpreter.
Uses `importlib.util.find_spec` for a lightweight check with no actual
imports. Call this in the app process *before* spawning the server
subprocess so users get a clear, actionable error instead of an opaque
server crash when the optional `quickjs` extra is not installed.
server crash when the core dependency is missing or broken.
Returns silently when the package is importable.
@@ -399,16 +398,16 @@ def verify_interpreter_deps() -> None:
from deepagents_code.config import _is_editable_install
if _is_editable_install():
from deepagents_code.update_check import editable_extra_hint
msg = (
"Missing dependencies for --interpreter. Editable install "
f"detected — {editable_extra_hint('quickjs')}"
"Missing core dependency for the interpreter. Editable install "
"detected — refresh the local environment with uv sync, or "
"relaunch with --no-interpreter to skip it."
)
else:
msg = (
"Missing dependencies for --interpreter. "
"Install with: dcode --install quickjs"
"Missing core dependency for the interpreter. "
"Reinstall dcode to restore langchain-quickjs, or relaunch with "
"--no-interpreter to skip it."
)
raise ImportError(msg)
+81 -24
View File
@@ -491,15 +491,57 @@ def _parse_interpreter_tools_flag(
return names
def _warn_if_interpreter_tools_without_interpreter(args: argparse.Namespace) -> None:
"""Warn that `--interpreter-tools` is a no-op without `--interpreter`.
def _resolve_interpreter_enabled(args: argparse.Namespace) -> bool:
"""Return whether the JS interpreter should run for these CLI args.
The PTC allowlist applies only when the interpreter middleware is enabled,
and on the CLI that gate is the `--interpreter` flag alone (`args.interpreter`).
`[interpreter]` config is not consulted: `config.toml`'s `enable_interpreter`
does not currently enable the middleware on this path, so a missing
`--interpreter` always means the flag has no effect. If that ever changes,
this check must consider config to avoid a false-positive warning.
Delegates to `_resolve_enable_interpreter` so the CLI pre-flight gate and the
stored `ServerConfig` share one resolution rule and cannot drift. The default
comes from `[interpreter].enable_interpreter` in local mode and is disabled
for remote sandboxes, where `CodeInterpreterMiddleware` is unsupported;
explicit `--interpreter`/`--no-interpreter` overrides the default.
`args.sandbox` is already normalized by `parse_args` (the bare-flag sentinel
is resolved to a provider name), so the resolver sees the concrete value.
"""
from deepagents_code._server_config import _resolve_enable_interpreter
return _resolve_enable_interpreter(args.interpreter, args.sandbox)
def _warn_if_interpreter_disabled_by_sandbox(args: argparse.Namespace) -> None:
"""Warn that a remote sandbox suppressed the otherwise-default interpreter.
With `js_eval` on by default in local mode, a `--sandbox` run silently drops
it (the middleware is unsupported under a remote sandbox). This prints to
stderr on the non-interactive (`-n`) path; the interactive TUI surfaces the
same advisory as a startup notification (see
`DeepAgentsApp._notify_interpreter_disabled_by_sandbox`).
Keyed on the raw `args.interpreter` tri-state so an explicit
`--no-interpreter` opt-out stays silent (the predicate only fires for the
unset default).
"""
from deepagents_code._server_config import _interpreter_suppressed_by_sandbox
from deepagents_code.config import settings
if not _interpreter_suppressed_by_sandbox(
enable_interpreter=args.interpreter,
sandbox_type=args.sandbox,
local_default=settings.enable_interpreter,
):
return
from rich.console import Console as _Console
_Console(stderr=True).print(
"[yellow]Warning:[/yellow] JS interpreter (`js_eval`) is unavailable "
"under a remote sandbox; it runs in local mode only."
)
def _warn_if_interpreter_tools_without_interpreter(
args: argparse.Namespace, *, enable_interpreter: bool
) -> None:
"""Warn that `--interpreter-tools` is a no-op without the interpreter.
This drives the non-interactive (`-n`) path and prints to stderr. The
interactive TUI surfaces the same advisory as a startup notification (see
@@ -510,13 +552,13 @@ def _warn_if_interpreter_tools_without_interpreter(args: argparse.Namespace) ->
"""
if args.interpreter_tools is None:
return
if args.interpreter:
if enable_interpreter:
return
from rich.console import Console as _Console
_Console(stderr=True).print(
"[yellow]Warning:[/yellow] --interpreter-tools has no effect "
"unless --interpreter is set."
"when the interpreter is disabled."
)
@@ -1462,9 +1504,10 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument(
"--interpreter",
action="store_true",
action=argparse.BooleanOptionalAction,
default=None,
help="Enable the JS interpreter (`js_eval`) middleware on the main agent. "
"Local mode only; requires the `quickjs` optional extra.",
"Enabled by default when not using a sandbox; use --no-interpreter to disable.",
)
parser.add_argument(
"--interpreter-tools",
@@ -1472,7 +1515,7 @@ def parse_args() -> argparse.Namespace:
metavar="VALUE",
help="PTC allowlist for `js_eval`: 'safe', 'all', or a comma-separated "
"list of tool names (which may include the 'safe' preset, e.g. "
"'safe,task'). Default is no PTC (pure REPL).",
"'safe,task'). Default is 'safe' (read-only file tools).",
)
parser.add_argument(
@@ -1493,7 +1536,7 @@ def parse_args() -> argparse.Namespace:
parser.add_argument(
"--install",
metavar="NAME",
help="Install an optional extra (e.g. quickjs, daytona, fireworks), then exit",
help="Install an optional extra (e.g. daytona, fireworks), then exit",
)
parser.add_argument(
"--package",
@@ -1643,7 +1686,8 @@ async def run_textual_cli_async(
mcp_config_path: str | None = None,
no_mcp: bool = False,
trust_project_mcp: bool | None = None,
enable_interpreter: bool = False,
enable_interpreter: bool | None = None,
interpreter_arg: bool | None = None,
interpreter_ptc: str | list[str] | None = None,
interpreter_ptc_acknowledge_unsafe: bool = False,
) -> "AppResult":
@@ -1692,7 +1736,11 @@ async def run_textual_cli_async(
`True` to allow, `False` to deny, `None` to check trust store.
enable_interpreter: Enable `CodeInterpreterMiddleware` (`js_eval`) on
the main agent. Local-mode only.
the main agent. `None` defers to the sandbox-aware/config default.
interpreter_arg: The raw `--interpreter`/`--no-interpreter` tri-state,
forwarded so the app can tell an explicit opt-out from a
sandbox-suppressed default when surfacing the disabled-by-sandbox
advisory.
interpreter_ptc: Override for `settings.interpreter_ptc` (PTC allowlist
for `js_eval`).
interpreter_ptc_acknowledge_unsafe: Explicit acknowledgement for
@@ -1802,6 +1850,7 @@ async def run_textual_cli_async(
mcp_preload_kwargs=mcp_preload_kwargs,
model_kwargs=model_kwargs,
model_explicitly_set=model_name is not None,
interpreter_arg=interpreter_arg,
defer_server_start=defer_server_start,
)
except Exception as e:
@@ -2244,11 +2293,13 @@ def _check_mcp_project_trust(*, trust_flag: bool = False) -> bool | None:
def _verify_interpreter_or_exit() -> None:
"""Run the `--interpreter` pre-flight check; print and exit on failure.
"""Run the interpreter pre-flight check; print and exit on failure.
Called before spawning the langgraph dev server subprocess so a missing
`langchain-quickjs` extra surfaces a one-line install hint instead of an
opaque "Server process exited with code N" downstream.
`langchain-quickjs` dependency surfaces a one-line, actionable hint instead
of an opaque "Server process exited with code N" downstream. Gated on the
resolved interpreter state (`_resolve_interpreter_enabled`), not the
`--interpreter` flag alone, since the interpreter is now on by default.
"""
from deepagents_code.extras_info import verify_interpreter_deps
@@ -3117,7 +3168,8 @@ def cli_main() -> None:
console.print(f"[bold red]Error:[/bold red] {escape(str(exc))}")
sys.exit(1)
if getattr(args, "interpreter", False):
enable_interpreter = _resolve_interpreter_enabled(args)
if enable_interpreter:
_verify_interpreter_or_exit()
# Non-interactive mode - execute single task and exit
@@ -3126,7 +3178,10 @@ def cli_main() -> None:
interpreter_ptc = _parse_interpreter_tools_flag(
getattr(args, "interpreter_tools", None)
)
_warn_if_interpreter_tools_without_interpreter(args)
_warn_if_interpreter_tools_without_interpreter(
args, enable_interpreter=enable_interpreter
)
_warn_if_interpreter_disabled_by_sandbox(args)
timeout = getattr(args, "timeout", None)
try:
@@ -3149,7 +3204,7 @@ def cli_main() -> None:
mcp_config_path=getattr(args, "mcp_config", None),
no_mcp=getattr(args, "no_mcp", False),
trust_project_mcp=getattr(args, "trust_project_mcp", False),
enable_interpreter=getattr(args, "interpreter", False),
enable_interpreter=enable_interpreter,
interpreter_ptc=interpreter_ptc,
max_turns=getattr(args, "max_turns", None),
),
@@ -3211,7 +3266,8 @@ def cli_main() -> None:
console.print(f"[bold red]Error:[/bold red] {escape(str(exc))}")
sys.exit(1)
if getattr(args, "interpreter", False):
enable_interpreter = _resolve_interpreter_enabled(args)
if enable_interpreter:
_verify_interpreter_or_exit()
# Check project MCP trust before launching TUI
@@ -3251,7 +3307,8 @@ def cli_main() -> None:
mcp_config_path=getattr(args, "mcp_config", None),
no_mcp=getattr(args, "no_mcp", False),
trust_project_mcp=mcp_trust_decision,
enable_interpreter=getattr(args, "interpreter", False),
enable_interpreter=enable_interpreter,
interpreter_arg=args.interpreter,
interpreter_ptc=interpreter_ptc,
)
)
+2 -2
View File
@@ -932,7 +932,7 @@ async def run_non_interactive(
mcp_config_path: str | None = None,
no_mcp: bool = False,
trust_project_mcp: bool = False,
enable_interpreter: bool = False,
enable_interpreter: bool | None = None,
interpreter_ptc: str | list[str] | None = None,
interpreter_ptc_acknowledge_unsafe: bool = False,
max_turns: int | None = None,
@@ -992,7 +992,7 @@ async def run_non_interactive(
servers. When `False` (default), project stdio servers are
silently skipped.
enable_interpreter: Enable the JS interpreter (`js_eval`) middleware
on the main agent. Local-mode only.
on the main agent. `None` uses the sandbox-aware default.
interpreter_ptc: Override for `settings.interpreter_ptc` (PTC
allowlist for `js_eval`).
interpreter_ptc_acknowledge_unsafe: Explicit acknowledgement for
+4 -4
View File
@@ -281,7 +281,7 @@ async def start_server_and_get_agent(
sandbox_setup: str | None = None,
enable_shell: bool = True,
enable_ask_user: bool = False,
enable_interpreter: bool = False,
enable_interpreter: bool | None = None,
interpreter_ptc: str | list[str] | None = None,
interpreter_ptc_acknowledge_unsafe: bool = False,
mcp_config_path: str | None = None,
@@ -307,7 +307,7 @@ async def start_server_and_get_agent(
enable_shell: Enable shell execution tools.
enable_ask_user: Enable ask_user tool.
enable_interpreter: Enable the JS interpreter (`js_eval`) middleware on
the main agent. Local-mode only.
the main agent. `None` uses the sandbox-aware default.
interpreter_ptc: Override for `settings.interpreter_ptc` (PTC allowlist).
interpreter_ptc_acknowledge_unsafe: Explicit acknowledgement for
`interpreter_ptc="all"` outside of `auto_approve`.
@@ -406,7 +406,7 @@ async def server_session(
sandbox_setup: str | None = None,
enable_shell: bool = True,
enable_ask_user: bool = False,
enable_interpreter: bool = False,
enable_interpreter: bool | None = None,
interpreter_ptc: str | list[str] | None = None,
interpreter_ptc_acknowledge_unsafe: bool = False,
mcp_config_path: str | None = None,
@@ -435,7 +435,7 @@ async def server_session(
enable_shell: Enable shell execution tools.
enable_ask_user: Enable ask_user tool.
enable_interpreter: Enable the JS interpreter (`js_eval`) middleware on
the main agent. Local-mode only.
the main agent. `None` uses the sandbox-aware default.
interpreter_ptc: Override for `settings.interpreter_ptc` (PTC allowlist).
interpreter_ptc_acknowledge_unsafe: Explicit acknowledgement for
`interpreter_ptc="all"` outside of `auto_approve`.
+6 -3
View File
@@ -167,8 +167,11 @@ def show_help() -> None:
" --trust-project-mcp Trust project MCP configs (skip approval prompt)"
)
console.print(
" --interpreter Enable JS interpreter (`js_eval`) middleware "
"(local mode only)"
" --interpreter, --no-interpreter"
" Enable or disable JS interpreter (`js_eval`) middleware"
)
console.print(
" Enabled by default when not using a sandbox"
)
console.print(
" --interpreter-tools VALUE PTC allowlist: 'safe', 'all', or comma-separated "
@@ -205,7 +208,7 @@ def show_help() -> None:
" --auto-update Toggle automatic updates on or off, then exit"
)
console.print(
" --install NAME Install an optional extra (e.g. quickjs)"
" --install NAME Install an optional extra (e.g. daytona)"
)
console.print(
" --package With --install, treat NAME as a package "
+2 -1
View File
@@ -52,10 +52,11 @@ _TIPS: dict[str, int] = {
"Press ctrl+u to delete to the start of the line in the chat input": 1,
"Use /skill:<name> to invoke a skill directly": 1,
"Type /update to check for and install updates": 1,
"Use /install <extra> to add optional dependencies (e.g. /install quickjs)": 1,
"Use /install <extra> to add optional dependencies (e.g. /install daytona)": 1,
"Use /theme to customize the TUI's colors": 1,
"In /theme, press N to toggle labels/keys, T to set for the current terminal": 1,
"Use /skill-creator to build reusable agent skills": 1,
"Ask for a workflow to fan work out to subagents in parallel": 3,
"Use /auto-update to toggle automatic updates": 1,
"Use /timestamps to show or hide message timestamp footers": 1,
"Use /agents to browse and switch between your available agents": 2,
+3 -3
View File
@@ -57,6 +57,7 @@ dependencies = [
# Tools
"tavily-python>=0.7.26,<1.0.0",
"langchain-quickjs>=0.3.1,<0.4.0",
# Clipboard
"pyperclip>=1.11.0,<2.0.0",
@@ -117,8 +118,8 @@ all-sandboxes = [
"deepagents-code[agentcore,daytona,modal,runloop,vercel]",
]
# Standalone integrations (not part of any composite extra)
quickjs = ["langchain-quickjs>=0.3.1,<0.4.0"]
# Standalone integrations (kept for backwards-compatible install commands)
quickjs = []
[project.scripts]
@@ -143,7 +144,6 @@ test = [
"hatchling>=1.27.0,<2.0.0",
"ruff>=0.15.18,<1.0.0",
"ty>=0.0.52,<1.0.0",
"langchain-quickjs>=0.3.1,<0.4.0",
"pytest>=9.1.1,<10.0.0",
"pytest-asyncio>=1.4.0,<2.0.0",
"pytest-benchmark>=5.2.3,<6.0.0",
+91 -1
View File
@@ -16980,7 +16980,7 @@ class TestNotifyInterpreterToolsWithoutInterpreter:
notify_mock.assert_called_once()
assert (
"--interpreter-tools has no effect unless --interpreter is set"
"--interpreter-tools has no effect when the interpreter is disabled"
in notify_mock.call_args.args[0]
)
assert notify_mock.call_args.kwargs.get("severity") == "warning"
@@ -17031,6 +17031,96 @@ class TestNotifyInterpreterToolsWithoutInterpreter:
notify_mock.assert_not_called()
class TestNotifyInterpreterDisabledBySandbox:
"""Tests for `_notify_interpreter_disabled_by_sandbox` (TUI advisory)."""
def test_toasts_when_sandbox_suppresses_default(self) -> None:
"""A remote sandbox with the unset, default-on interpreter warns once."""
from deepagents_code.config import settings
app = DeepAgentsApp(
server_kwargs={
"assistant_id": "agent",
"model_name": None,
"sandbox_type": "daytona",
"enable_interpreter": False,
},
interpreter_arg=None,
)
notify_mock = MagicMock()
app.notify = notify_mock # ty: ignore
with patch.object(settings, "enable_interpreter", True):
app._notify_interpreter_disabled_by_sandbox()
notify_mock.assert_called_once()
assert "unavailable under a remote sandbox" in notify_mock.call_args.args[0]
assert notify_mock.call_args.kwargs.get("severity") == "warning"
assert notify_mock.call_args.kwargs.get("markup") is False
def test_no_toast_in_local_mode(self) -> None:
"""Local mode keeps the interpreter, so there is nothing to warn about."""
from deepagents_code.config import settings
app = DeepAgentsApp(
server_kwargs={
"assistant_id": "agent",
"model_name": None,
"enable_interpreter": True,
},
interpreter_arg=None,
)
notify_mock = MagicMock()
app.notify = notify_mock # ty: ignore
with patch.object(settings, "enable_interpreter", True):
app._notify_interpreter_disabled_by_sandbox()
notify_mock.assert_not_called()
def test_no_toast_on_explicit_opt_out(self) -> None:
"""An explicit `--no-interpreter` opt-out under a sandbox is not announced."""
from deepagents_code.config import settings
app = DeepAgentsApp(
server_kwargs={
"assistant_id": "agent",
"model_name": None,
"sandbox_type": "daytona",
"enable_interpreter": False,
},
interpreter_arg=False,
)
notify_mock = MagicMock()
app.notify = notify_mock # ty: ignore
with patch.object(settings, "enable_interpreter", True):
app._notify_interpreter_disabled_by_sandbox()
notify_mock.assert_not_called()
def test_no_toast_when_config_default_off(self) -> None:
"""A user who disabled the interpreter in config is not nagged."""
from deepagents_code.config import settings
app = DeepAgentsApp(
server_kwargs={
"assistant_id": "agent",
"model_name": None,
"sandbox_type": "daytona",
"enable_interpreter": False,
},
interpreter_arg=None,
)
notify_mock = MagicMock()
app.notify = notify_mock # ty: ignore
with patch.object(settings, "enable_interpreter", False):
app._notify_interpreter_disabled_by_sandbox()
notify_mock.assert_not_called()
class TestNotifyOrphanedTracingDisabled:
"""Tests for `_notify_orphaned_tracing_disabled` (TUI advisory)."""
+4 -4
View File
@@ -4745,12 +4745,12 @@ class TestInterpreterSettings:
with patch.object(model_config, "DEFAULT_CONFIG_PATH", config_path):
settings_obj = Settings.from_environment(start_path=tmp_path)
assert settings_obj.enable_interpreter is False
assert settings_obj.enable_interpreter is True
assert settings_obj.interpreter_timeout_seconds == pytest.approx(5.0)
assert settings_obj.interpreter_memory_limit_mb == 64
assert settings_obj.interpreter_max_ptc_calls == 256
assert settings_obj.interpreter_max_result_chars == 4000
assert settings_obj.interpreter_ptc is False
assert settings_obj.interpreter_ptc == "safe"
assert settings_obj.interpreter_ptc_acknowledge_unsafe is False
def test_round_trip_through_toml(self, tmp_path: Path) -> None:
@@ -4802,7 +4802,7 @@ ptc = [""]
with patch.object(model_config, "DEFAULT_CONFIG_PATH", config_path):
settings_obj = Settings.from_environment(start_path=tmp_path)
assert settings_obj.interpreter_ptc is False
assert settings_obj.interpreter_ptc == "safe"
def test_ptc_list_with_safe_preset_round_trip(self, tmp_path: Path) -> None:
"""`"safe"` is preserved as a list entry until agent-build expansion."""
@@ -4830,7 +4830,7 @@ ptc = ["all", "task"]
with patch.object(model_config, "DEFAULT_CONFIG_PATH", config_path):
settings_obj = Settings.from_environment(start_path=tmp_path)
assert settings_obj.interpreter_ptc is False
assert settings_obj.interpreter_ptc == "safe"
class TestCreateModelCodex:
@@ -310,26 +310,26 @@ def test_format_known_extras_groups_extras_under_correct_label() -> None:
# `deepagents_code.config` at call time. Patch the source module — patching
# `deepagents_code.extras_info._is_editable_install` would not work (it isn't
# bound there as a module-level attribute).
def test_verify_interpreter_deps_raises_with_dcode_hint_for_tool_install() -> None:
def test_verify_interpreter_deps_raises_with_reinstall_hint_for_tool_install() -> None:
with (
patch(
"deepagents_code.extras_info.importlib.util.find_spec", return_value=None
),
patch("deepagents_code.config._is_editable_install", return_value=False),
pytest.raises(ImportError, match="dcode --install quickjs"),
pytest.raises(ImportError, match="Reinstall dcode"),
):
verify_interpreter_deps()
def test_verify_interpreter_deps_raises_with_uv_hint_for_editable_install() -> None:
def test_verify_interpreter_deps_raises_with_uv_sync_hint_for_editable_install() -> (
None
):
with (
patch(
"deepagents_code.extras_info.importlib.util.find_spec", return_value=None
),
patch("deepagents_code.config._is_editable_install", return_value=True),
pytest.raises(
ImportError, match=r"uv tool install --editable.*deepagents-code\[quickjs\]"
),
pytest.raises(ImportError, match="uv sync"),
):
verify_interpreter_deps()
@@ -194,14 +194,8 @@ async def test_install_slash_provider_extra_skips_redundant_hint_when_prompted()
assert "/restart" not in contents
async def test_install_slash_standalone_extra_recommends_interpreter_relaunch() -> None:
"""`quickjs` must point at a `--interpreter` relaunch, not `/restart`.
`quickjs` only does anything behind the launch-only `--interpreter` flag
(with a startup-time dependency gate), so a `/restart` which reuses the
original launch settings can't enable it. The user has to exit and
relaunch with the flag.
"""
async def test_install_slash_standalone_extra_recommends_relaunch() -> None:
"""Compatibility standalone extras still point at a full relaunch."""
app = DeepAgentsApp()
async with app.run_test() as pilot:
await pilot.pause()
@@ -222,7 +216,7 @@ async def test_install_slash_standalone_extra_recommends_interpreter_relaunch()
rendered = str(success._content)
assert "/restart" not in rendered
assert "relaunch dcode" in rendered.lower()
assert "--interpreter" in rendered
assert "--interpreter" not in rendered
async def test_install_slash_unknown_extra_requires_force() -> None:
@@ -321,7 +321,7 @@ class TestLaunchDependenciesScreen:
assert max_height.cells < 16
async def test_renders_other_category(self) -> None:
"""Standalone extras (e.g. quickjs) get their own category."""
"""Compatibility standalone extras get their own category."""
statuses = (
ExtraDependencyStatus(
name="quickjs", installed=(), missing=("langchain-quickjs",)
+308 -10
View File
@@ -2204,22 +2204,151 @@ class TestParseInterpreterToolsFlag:
assert exc_info.value.code == 2
class TestInterpreterFlagParsing:
"""`--interpreter` is a tri-state `BooleanOptionalAction` (default `None`)."""
def test_default_is_none(self, mock_argv: MockArgvType) -> None:
with mock_argv("-n", "task"):
args = parse_args()
assert args.interpreter is None
def test_explicit_flag_is_true(self, mock_argv: MockArgvType) -> None:
with mock_argv("-n", "task", "--interpreter"):
args = parse_args()
assert args.interpreter is True
def test_no_flag_is_false(self, mock_argv: MockArgvType) -> None:
with mock_argv("-n", "task", "--no-interpreter"):
args = parse_args()
assert args.interpreter is False
class TestResolveInterpreterEnabled:
"""Tests for `_resolve_interpreter_enabled`."""
def test_local_mode_uses_config_default(self, mock_argv: MockArgvType) -> None:
from deepagents_code.config import settings
from deepagents_code.main import _resolve_interpreter_enabled
with mock_argv("-n", "task"):
args = parse_args()
with patch.object(settings, "enable_interpreter", False):
assert _resolve_interpreter_enabled(args) is False
with patch.object(settings, "enable_interpreter", True):
assert _resolve_interpreter_enabled(args) is True
def test_sandbox_defaults_disabled(self, mock_argv: MockArgvType) -> None:
from deepagents_code.config import settings
from deepagents_code.main import _resolve_interpreter_enabled
with mock_argv("-n", "task", "--sandbox", "daytona"):
args = parse_args()
with patch.object(settings, "enable_interpreter", True):
assert _resolve_interpreter_enabled(args) is False
def test_explicit_flag_overrides_sandbox_default(
self, mock_argv: MockArgvType
) -> None:
from deepagents_code.main import _resolve_interpreter_enabled
with mock_argv("-n", "task", "--sandbox", "daytona", "--interpreter"):
args = parse_args()
assert _resolve_interpreter_enabled(args) is True
def test_explicit_no_flag_overrides_local_default(
self, mock_argv: MockArgvType
) -> None:
from deepagents_code.config import settings
from deepagents_code.main import _resolve_interpreter_enabled
with mock_argv("-n", "task", "--no-interpreter"):
args = parse_args()
with patch.object(settings, "enable_interpreter", True):
assert _resolve_interpreter_enabled(args) is False
def test_empty_sandbox_treated_as_local(self) -> None:
"""An empty-string sandbox is falsy and must resolve as local mode.
Parity with the `_resolve_enable_interpreter` edge case (the CLI resolver
delegates to it); a regression in the falsy-sandbox guard must fail here
too, not only at the server-config layer.
"""
import argparse
from deepagents_code.config import settings
from deepagents_code.main import _resolve_interpreter_enabled
args = argparse.Namespace(interpreter=None, sandbox="")
with patch.object(settings, "enable_interpreter", True):
assert _resolve_interpreter_enabled(args) is True
class TestRunTextualCliAsyncInterpreterDefault:
"""Tests for TUI helper interpreter tri-state forwarding."""
@pytest.mark.parametrize(
("value", "expected"),
[
(None, None),
(True, True),
(False, False),
],
)
async def test_forwards_interpreter_tri_state(
self,
monkeypatch: pytest.MonkeyPatch,
value: bool | None,
expected: bool | None,
) -> None:
from deepagents_code.app import AppResult
from deepagents_code.main import run_textual_cli_async
run_textual_app = AsyncMock(
return_value=AppResult(return_code=0, thread_id="thread")
)
monkeypatch.setattr(
"deepagents_code.config._get_default_model_spec",
lambda: "test-model",
)
monkeypatch.setattr("deepagents_code.config.detect_provider", lambda _: "")
monkeypatch.setattr(
"deepagents_code.onboarding.should_run_onboarding",
lambda: False,
)
monkeypatch.setattr("deepagents_code.app.run_textual_app", run_textual_app)
if value is not None:
await run_textual_cli_async(
assistant_id="agent",
sandbox_type="daytona",
enable_interpreter=value,
)
else:
await run_textual_cli_async(
assistant_id="agent",
sandbox_type="daytona",
)
server_kwargs = run_textual_app.call_args.kwargs["server_kwargs"]
assert server_kwargs["enable_interpreter"] is expected
class TestWarnInterpreterToolsWithoutInterpreter:
"""Tests for `_warn_if_interpreter_tools_without_interpreter`."""
def test_warns_without_interpreter(
def test_warns_when_interpreter_disabled(
self, mock_argv: MockArgvType, capsys: pytest.CaptureFixture[str]
) -> None:
"""--interpreter-tools without --interpreter warns and does not exit."""
"""--interpreter-tools with --no-interpreter warns and does not exit."""
from deepagents_code.main import (
_warn_if_interpreter_tools_without_interpreter,
)
with mock_argv("-n", "task", "--interpreter-tools", "safe"):
with mock_argv("-n", "task", "--no-interpreter", "--interpreter-tools", "safe"):
args = parse_args()
_warn_if_interpreter_tools_without_interpreter(args)
_warn_if_interpreter_tools_without_interpreter(args, enable_interpreter=False)
assert (
"--interpreter-tools has no effect unless --interpreter is set"
"--interpreter-tools has no effect when the interpreter is disabled"
in capsys.readouterr().err
)
@@ -2233,7 +2362,7 @@ class TestWarnInterpreterToolsWithoutInterpreter:
with mock_argv("-n", "task", "--interpreter", "--interpreter-tools", "safe"):
args = parse_args()
_warn_if_interpreter_tools_without_interpreter(args)
_warn_if_interpreter_tools_without_interpreter(args, enable_interpreter=True)
assert capsys.readouterr().err == ""
def test_no_warning_without_flag(
@@ -2246,13 +2375,13 @@ class TestWarnInterpreterToolsWithoutInterpreter:
with mock_argv("-n", "task"):
args = parse_args()
_warn_if_interpreter_tools_without_interpreter(args)
_warn_if_interpreter_tools_without_interpreter(args, enable_interpreter=True)
assert capsys.readouterr().err == ""
def test_cli_main_emits_warning_on_non_interactive_path(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""End-to-end: `-n --interpreter-tools` without `--interpreter` warns.
"""End-to-end: `-n --no-interpreter --interpreter-tools` warns.
Guards the wiring (not just the helper): a dropped or misplaced call
site, or an earlier `sys.exit` swallowing the warning, fails here.
@@ -2263,7 +2392,16 @@ class TestWarnInterpreterToolsWithoutInterpreter:
mock_stdin.isatty.return_value = True
with (
patch.object(
sys, "argv", ["deepagents", "-n", "task", "--interpreter-tools", "safe"]
sys,
"argv",
[
"deepagents",
"-n",
"task",
"--no-interpreter",
"--interpreter-tools",
"safe",
],
),
patch.object(sys, "stdin", mock_stdin),
patch("deepagents_code.main.check_optional_tools", return_value=[]),
@@ -2282,6 +2420,166 @@ class TestWarnInterpreterToolsWithoutInterpreter:
assert exc_info.value.code == 0
assert (
"--interpreter-tools has no effect unless --interpreter is set"
"--interpreter-tools has no effect when the interpreter is disabled"
in capsys.readouterr().err
)
class TestWarnInterpreterDisabledBySandbox:
"""Tests for `_warn_if_interpreter_disabled_by_sandbox` (stderr advisory)."""
def test_warns_when_sandbox_suppresses_default(
self, mock_argv: MockArgvType, capsys: pytest.CaptureFixture[str]
) -> None:
"""A `--sandbox` run with the default-on interpreter warns on stderr."""
from deepagents_code.config import settings
from deepagents_code.main import _warn_if_interpreter_disabled_by_sandbox
with mock_argv("-n", "task", "--sandbox", "daytona"):
args = parse_args()
with patch.object(settings, "enable_interpreter", True):
_warn_if_interpreter_disabled_by_sandbox(args)
assert "unavailable under a remote sandbox" in capsys.readouterr().err
def test_silent_in_local_mode(
self, mock_argv: MockArgvType, capsys: pytest.CaptureFixture[str]
) -> None:
"""Local mode keeps the interpreter, so there is nothing to warn about."""
from deepagents_code.config import settings
from deepagents_code.main import _warn_if_interpreter_disabled_by_sandbox
with mock_argv("-n", "task"):
args = parse_args()
with patch.object(settings, "enable_interpreter", True):
_warn_if_interpreter_disabled_by_sandbox(args)
assert capsys.readouterr().err == ""
def test_silent_on_explicit_opt_out_under_sandbox(
self, mock_argv: MockArgvType, capsys: pytest.CaptureFixture[str]
) -> None:
"""An explicit `--no-interpreter` is the user's choice, not a drop."""
from deepagents_code.config import settings
from deepagents_code.main import _warn_if_interpreter_disabled_by_sandbox
with mock_argv("-n", "task", "--sandbox", "daytona", "--no-interpreter"):
args = parse_args()
with patch.object(settings, "enable_interpreter", True):
_warn_if_interpreter_disabled_by_sandbox(args)
assert capsys.readouterr().err == ""
def test_silent_when_config_default_off(
self, mock_argv: MockArgvType, capsys: pytest.CaptureFixture[str]
) -> None:
"""A user who disabled the interpreter in config is not nagged."""
from deepagents_code.config import settings
from deepagents_code.main import _warn_if_interpreter_disabled_by_sandbox
with mock_argv("-n", "task", "--sandbox", "daytona"):
args = parse_args()
with patch.object(settings, "enable_interpreter", False):
_warn_if_interpreter_disabled_by_sandbox(args)
assert capsys.readouterr().err == ""
def test_cli_main_forwards_enabled_interpreter_in_local_mode(self) -> None:
"""End-to-end: bare `-n task` resolves the interpreter on and forwards it.
Guards the `cli_main` -> `run_non_interactive` wiring: a dropped or
reverted assignment (e.g. back to a hard-coded `False`) fails here.
"""
from deepagents_code.config import settings
from deepagents_code.main import cli_main
run_mock = AsyncMock(return_value=0)
mock_stdin = MagicMock()
mock_stdin.isatty.return_value = True
with (
patch.object(sys, "argv", ["deepagents", "-n", "task"]),
patch.object(sys, "stdin", mock_stdin),
patch("deepagents_code.main.check_optional_tools", return_value=[]),
patch(
"deepagents_code.main._should_ensure_managed_ripgrep",
return_value=False,
),
patch.object(settings, "enable_interpreter", True),
patch("deepagents_code.non_interactive.run_non_interactive", run_mock),
pytest.raises(SystemExit) as exc_info,
):
cli_main()
assert exc_info.value.code == 0
assert run_mock.call_args.kwargs["enable_interpreter"] is True
def test_cli_main_warns_and_disables_under_sandbox(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""End-to-end: `-n --sandbox` forwards the disabled interpreter and warns."""
from deepagents_code.config import settings
from deepagents_code.main import cli_main
run_mock = AsyncMock(return_value=0)
mock_stdin = MagicMock()
mock_stdin.isatty.return_value = True
with (
patch.object(
sys, "argv", ["deepagents", "-n", "task", "--sandbox", "daytona"]
),
patch.object(sys, "stdin", mock_stdin),
patch("deepagents_code.main.check_optional_tools", return_value=[]),
patch(
"deepagents_code.main._should_ensure_managed_ripgrep",
return_value=False,
),
patch(
"deepagents_code.integrations.sandbox_factory.verify_sandbox_deps",
),
patch.object(settings, "enable_interpreter", True),
patch("deepagents_code.non_interactive.run_non_interactive", run_mock),
pytest.raises(SystemExit) as exc_info,
):
cli_main()
assert exc_info.value.code == 0
assert run_mock.call_args.kwargs["enable_interpreter"] is False
assert "unavailable under a remote sandbox" in capsys.readouterr().err
def test_cli_main_silent_on_explicit_opt_out_under_sandbox(
self, capsys: pytest.CaptureFixture[str]
) -> None:
"""End-to-end: `-n --sandbox --no-interpreter` disables without an advisory."""
from deepagents_code.config import settings
from deepagents_code.main import cli_main
run_mock = AsyncMock(return_value=0)
mock_stdin = MagicMock()
mock_stdin.isatty.return_value = True
with (
patch.object(
sys,
"argv",
[
"deepagents",
"-n",
"task",
"--sandbox",
"daytona",
"--no-interpreter",
],
),
patch.object(sys, "stdin", mock_stdin),
patch("deepagents_code.main.check_optional_tools", return_value=[]),
patch(
"deepagents_code.main._should_ensure_managed_ripgrep",
return_value=False,
),
patch(
"deepagents_code.integrations.sandbox_factory.verify_sandbox_deps",
),
patch.object(settings, "enable_interpreter", True),
patch("deepagents_code.non_interactive.run_non_interactive", run_mock),
pytest.raises(SystemExit) as exc_info,
):
cli_main()
assert exc_info.value.code == 0
assert run_mock.call_args.kwargs["enable_interpreter"] is False
assert "unavailable under a remote sandbox" not in capsys.readouterr().err
@@ -304,6 +304,7 @@ class TestSandboxTypeForwarding:
_, kwargs = mock_start_server.call_args
assert kwargs["sandbox_type"] == "modal"
assert kwargs["enable_interpreter"] is None
async def test_sandbox_snapshot_name_passed_to_server(self) -> None:
"""`sandbox_snapshot_name` must reach `start_server_and_get_agent`."""
@@ -11,12 +11,14 @@ import pytest
from deepagents_code._env_vars import SERVER_ENV_PREFIX
from deepagents_code._server_config import (
ServerConfig,
_interpreter_suppressed_by_sandbox,
_normalize_path,
_read_env_bool,
_read_env_json,
_read_env_optional_bool,
_read_env_str,
)
from deepagents_code.config import settings
# ------------------------------------------------------------------
# _read_env_bool
@@ -186,6 +188,126 @@ class TestServerConfigPostInit:
assert config.sandbox_type is None
class TestServerConfigInterpreterDefault:
"""Tests for sandbox-aware interpreter default resolution."""
def test_bare_server_config_keeps_interpreter_disabled(self) -> None:
config = ServerConfig()
assert config.enable_interpreter is False
@staticmethod
def _build(*, sandbox_type: str, enable_interpreter: bool | None) -> ServerConfig:
"""Build a `ServerConfig` exercising only the interpreter resolution."""
return ServerConfig.from_cli_args(
project_context=None,
model_name=None,
model_params=None,
assistant_id="agent",
auto_approve=False,
sandbox_type=sandbox_type,
sandbox_id=None,
sandbox_snapshot_name=None,
sandbox_setup=None,
enable_shell=True,
enable_ask_user=False,
enable_interpreter=enable_interpreter,
mcp_config_path=None,
no_mcp=False,
trust_project_mcp=None,
interactive=True,
)
def test_local_none_false_uses_settings_default(self) -> None:
with patch.object(settings, "enable_interpreter", False):
config = self._build(sandbox_type="none", enable_interpreter=None)
assert config.enable_interpreter is False
def test_local_none_true_uses_settings_default(self) -> None:
with patch.object(settings, "enable_interpreter", True):
config = self._build(sandbox_type="none", enable_interpreter=None)
assert config.enable_interpreter is True
def test_local_explicit_false_is_preserved(self) -> None:
# An explicit `False` must win over a `True` config default rather than
# falling through to the settings lookup.
with patch.object(settings, "enable_interpreter", True):
config = self._build(sandbox_type="none", enable_interpreter=False)
assert config.enable_interpreter is False
def test_empty_sandbox_is_treated_as_local(self) -> None:
# An empty-string sandbox is falsy and must not be mistaken for a remote
# backend, which would silently disable the interpreter.
with patch.object(settings, "enable_interpreter", True):
config = self._build(sandbox_type="", enable_interpreter=None)
assert config.enable_interpreter is True
def test_remote_none_disables_interpreter(self) -> None:
with patch.object(settings, "enable_interpreter", True):
config = self._build(sandbox_type="daytona", enable_interpreter=None)
assert config.enable_interpreter is False
def test_remote_explicit_true_is_preserved_for_validation(self) -> None:
config = self._build(sandbox_type="daytona", enable_interpreter=True)
assert config.enable_interpreter is True
class TestInterpreterSuppressedBySandbox:
"""Tests for the `_interpreter_suppressed_by_sandbox` advisory predicate.
The predicate takes the *raw* tri-state intent: only the unset default
(`None`) can be silently suppressed by a sandbox.
"""
def test_suppressed_when_remote_and_default_on(self) -> None:
# Unset intent + remote sandbox + default-on = a silent drop worth a heads-up.
assert _interpreter_suppressed_by_sandbox(
enable_interpreter=None, sandbox_type="daytona", local_default=True
)
def test_not_suppressed_on_explicit_enable(self) -> None:
# `--interpreter` on a sandbox is the user's choice; the server raises a
# clear error instead of a silent drop.
assert not _interpreter_suppressed_by_sandbox(
enable_interpreter=True, sandbox_type="daytona", local_default=True
)
def test_not_suppressed_on_explicit_opt_out(self) -> None:
# `--no-interpreter` is an explicit opt-out, not a sandbox-imposed drop.
assert not _interpreter_suppressed_by_sandbox(
enable_interpreter=False, sandbox_type="daytona", local_default=True
)
def test_not_suppressed_when_local(self) -> None:
assert not _interpreter_suppressed_by_sandbox(
enable_interpreter=None, sandbox_type=None, local_default=True
)
def test_not_suppressed_when_sandbox_none_string(self) -> None:
assert not _interpreter_suppressed_by_sandbox(
enable_interpreter=None, sandbox_type="none", local_default=True
)
def test_empty_sandbox_treated_as_local(self) -> None:
# An empty-string sandbox is falsy and must count as local, so the
# advisory does not fire spuriously.
assert not _interpreter_suppressed_by_sandbox(
enable_interpreter=None, sandbox_type="", local_default=True
)
def test_not_suppressed_when_default_off(self) -> None:
# A user who disabled the interpreter in config should not be nagged.
assert not _interpreter_suppressed_by_sandbox(
enable_interpreter=None, sandbox_type="daytona", local_default=False
)
# ------------------------------------------------------------------
# ServerConfig round-trip edge cases
# ------------------------------------------------------------------
@@ -204,6 +326,35 @@ class TestServerConfigEdgeCases:
assert restored.trust_project_mcp is False
def test_enable_interpreter_true_round_trips(self) -> None:
"""A resolved-`True` interpreter must survive the env boundary.
The "on by default" intent rides entirely on this round-trip: the
dataclass and `from_env` defaults are both `False`, so if `to_env` ever
dropped the key the subprocess would silently disable `js_eval`.
"""
original = ServerConfig(enable_interpreter=True)
env_dict = original.to_env()
with patch.dict(os.environ, {}, clear=True):
for suffix, value in env_dict.items():
if value is not None:
os.environ[f"{SERVER_ENV_PREFIX}{suffix}"] = value
restored = ServerConfig.from_env()
assert restored.enable_interpreter is True
def test_enable_interpreter_false_round_trips(self) -> None:
"""A resolved-`False` interpreter must survive (not flip to the default)."""
original = ServerConfig(enable_interpreter=False)
env_dict = original.to_env()
with patch.dict(os.environ, {}, clear=True):
for suffix, value in env_dict.items():
if value is not None:
os.environ[f"{SERVER_ENV_PREFIX}{suffix}"] = value
restored = ServerConfig.from_env()
assert restored.enable_interpreter is False
def test_sandbox_type_none_string_round_trips(self) -> None:
"""sandbox_type='none' normalizes to None and survives round-trip."""
original = ServerConfig(sandbox_type="none")
@@ -628,6 +628,11 @@ class TestBuildWelcomeFooter:
"""The `/copy` command must have a discoverability tip."""
assert "Use /copy to copy the latest assistant message" in _TIPS
def test_workflow_subagent_tip_registered(self) -> None:
"""The workflow trigger phrase should have a weighted discoverability tip."""
tip = "Ask for a workflow to fan work out to subagents in parallel"
assert _TIPS[tip] == 3
def test_tip_varies_across_calls(self) -> None:
"""Tips should rotate (not always the same)."""
seen = {build_welcome_footer().plain for _ in range(50)}
+2 -6
View File
@@ -1090,6 +1090,7 @@ dependencies = [
{ name = "langchain-google-genai" },
{ name = "langchain-mcp-adapters" },
{ name = "langchain-openai" },
{ name = "langchain-quickjs" },
{ name = "langgraph-checkpoint-sqlite" },
{ name = "langgraph-cli", extra = ["inmem"] },
{ name = "langgraph-runtime-inmem" },
@@ -1203,9 +1204,6 @@ openrouter = [
perplexity = [
{ name = "langchain-perplexity" },
]
quickjs = [
{ name = "langchain-quickjs" },
]
runloop = [
{ name = "langchain-runloop" },
]
@@ -1226,7 +1224,6 @@ xai = [
test = [
{ name = "build" },
{ name = "hatchling" },
{ name = "langchain-quickjs" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-benchmark" },
@@ -1278,7 +1275,7 @@ requires-dist = [
{ name = "langchain-openai", marker = "extra == 'openai'", specifier = ">=1.3.3,<2.0.0" },
{ name = "langchain-openrouter", marker = "extra == 'openrouter'", specifier = ">=0.2.4,<2.0.0" },
{ name = "langchain-perplexity", marker = "extra == 'perplexity'", specifier = ">=1.4.0,<2.0.0" },
{ name = "langchain-quickjs", marker = "extra == 'quickjs'", editable = "../partners/quickjs" },
{ name = "langchain-quickjs", editable = "../partners/quickjs" },
{ name = "langchain-runloop", marker = "extra == 'runloop'", editable = "../partners/runloop" },
{ name = "langchain-together", marker = "extra == 'together'", specifier = ">=0.4.0,<2.0.0" },
{ name = "langchain-vercel-sandbox", marker = "extra == 'vercel'", editable = "../partners/vercel" },
@@ -1311,7 +1308,6 @@ provides-extras = ["anthropic", "baseten", "bedrock", "cohere", "deepseek", "fir
test = [
{ name = "build", specifier = ">=1.5.0,<2.0.0" },
{ name = "hatchling", specifier = ">=1.27.0,<2.0.0" },
{ name = "langchain-quickjs", editable = "../partners/quickjs" },
{ name = "pytest", specifier = ">=9.1.1,<10.0.0" },
{ name = "pytest-asyncio", specifier = ">=1.4.0,<2.0.0" },
{ name = "pytest-benchmark", specifier = ">=5.2.3,<6.0.0" },