mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 09:45:24 -04:00
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:
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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 "
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
|
||||
@@ -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",)
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Generated
+2
-6
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user