fix(cli): eliminate trace fragmentation in non-interactive mode#2136
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
There was a problem hiding this comment.
Security Issues
- Privilege Escalation via HITL Bypass for Non-Shell Tools
When interrupt_shell_only is enabled, create_cli_agent sets interrupt_on = {} whenever the ShellAllowListMiddleware is added. This disables all Human-in-the-Loop (HITL) interrupts for every tool, not just shell execution. As a result, destructive/non-read-only tools (e.g., file writes/edits, network/HTTP requests) can execute automatically without user approval. This violates the project’s guardrail 8 (Enforce HITL approval for privileged operations) and creates a realistic path for an LLM-controlled agent to perform sensitive operations without approval in non-interactive mode.
Recommendations
- Keep HITL enforced for non-shell destructive tools even when using ShellAllowListMiddleware. Only avoid HITL interrupts for shell commands that the middleware is validating, and continue to require InterruptOnConfig-based HITL for write/execute/network and other sensitive tools.
| # No interrupts - all tools run automatically | ||
| if auto_approve or shell_middleware_added: # noqa: SIM108 # if-else clearer than ternary for dual-path config | ||
| # No HITL interrupts — tools run automatically. | ||
| # When shell_middleware_added is True, shell validation is handled by |
There was a problem hiding this comment.
Disabling all HITL interrupts when ShellAllowListMiddleware is added allows non-shell destructive tools to run automatically without approval, violating guardrail 8 and enabling privilege escalation in non-interactive flows.
Vulnerable code:
# Configure interrupt_on based on auto_approve / shell_middleware_added
interrupt_on: dict[str, bool | InterruptOnConfig] | None = None
if auto_approve or shell_middleware_added: # noqa: SIM108 # if-else clearer than ternary for dual-path config
# No HITL interrupts — tools run automatically.
# When shell_middleware_added is True, shell validation is handled by
# ShellAllowListMiddleware (added above) which rejects disallowed
# commands inline as error ToolMessages, keeping the entire run in
# a single LangSmith trace.
interrupt_on = {}Impact:
- Non-shell privileged operations (e.g., file write/edit, network requests) will execute without any HITL approval when
interrupt_shell_only=True, enabling an agent to perform sensitive actions without user consent.
Context:
- Project guardrail 8 requires HITL for privileged operations and cautions against bypassing the HITL middleware for write/execute/network actions.
Remediation:
- Retain HITL for non-shell destructive tools. Only avoid HITL interrupts for shell commands by validating them through
ShellAllowListMiddleware, while keepinginterrupt_onconfigured to require approval for other sensitive tools.
For more details, see the finding in Corridor.
Provide feedback: Reply with whether this is a valid vulnerability or false positive to help improve Corridor's accuracy.
There was a problem hiding this comment.
False positive, non-shell auto-approval is pre-existing documented behavior
There was a problem hiding this comment.
Thanks for the clarification. I’ve marked this finding as a false positive based on your feedback. Understood that non-shell auto-approval is intentional and documented pre-existing behavior, not introduced by this PR.
…chain-ai#2136) Non-interactive mode with a restrictive `--shell-allow-list` used the HITL interrupt/resume path to vet shell commands. That made the CLI do the right shell gating, but every allowed shell command still paused the graph and resumed it with `Command(resume=...)`. In LangSmith, that split one CLI task into multiple trace runs under the same thread. - Added `interrupt_shell_only` to the CLI -> server config path for non-interactive sessions that use a restrictive shell allow-list - Added `ShellAllowListMiddleware` to validate shell commands inline and return an error `ToolMessage` for rejected commands instead of pausing the graph - Applied the same shell-validation middleware to the top-level agent and delegated subagents - Explicitly inject our own `general-purpose` subagent when this mode is active so the SDK's default general-purpose subagent does not bypass the allow-list Before: - Non-interactive mode with `--shell-allow-list recommended` or an explicit list gated shell commands through HITL - Allowed shell commands still interrupted and resumed the graph - One CLI task could produce several LangSmith trace runs After: - Restrictive shell mode validates shell commands inline without `interrupt()` / `Command(resume=...)` - Allowed shell commands execute as part of one continuous run - Rejected shell commands return an error `ToolMessage` immediately - Delegated subagents use the same shell allow-list enforcement path
> [!CAUTION] > Merging this PR will automatically publish to **PyPI** and create a **GitHub release**. For the full release process, see [`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md). --- _Everything below this line will be the GitHub release body._ --- ## [0.0.35](deepagents-cli==0.0.34...deepagents-cli==0.0.35) (2026-04-07) ### Highlights * **Skills:** Invoke SDK skills directly from the CLI with `/skill:name` or at startup via `--skill`. Skills are a composable extension point for domain-specific agent behaviors. * **Themes & configuration:** New theme system with color overrides, global dotenv at `~/.deepagents/.env`, and a `DEEPAGENTS_CLI_` env var prefix for conflict-free configuration. * **Auto-updates:** Full update lifecycle — `/update` to upgrade in-place, `/auto-update` to toggle background checks, and a refreshed install script UX. * **Headless workflows:** Agent-friendly UX for scripted/headless invocations, AgentCore Code Interpreter sandbox provider, and improved non-interactive tracing and guidance. * **Performance:** Sub-250ms first paint via aggressive import deferral (pydantic, adapters, heavy SDK modules), markdown stack prewarming, and reduced health-poll intervals. ### Features * Load `~/.deepagents/.env` as global dotenv ([#1909](#1909)) ([5a21d0a](5a21d0a)) * Skill invocation via `/skill:name` ([#2037](#2037)) ([cc8cce7](cc8cce7)) * `--skill` startup invocation ([#2477](#2477)) ([5f0f1d4](5f0f1d4)) * Auto-update lifecycle, `/update` command, install script ux ([#2095](#2095)) ([fd92f6e](fd92f6e)) * `/auto-update` to toggle auto-updates ([#2276](#2276)) ([ad70bde](ad70bde)) * Themes ([#2134](#2134)) ([db67af0](db67af0)) * Allow color overrides on built-in themes, default dark to false ([#2275](#2275)) ([8f71865](8f71865)) * Add `DEEPAGENTS_CLI_` env var prefix and fix dotenv load order ([#2303](#2303)) ([29647bb](29647bb)) * Add `ls_integration` metadata to langsmith traces ([#2272](#2272)) ([5dd8098](5dd8098)) * Add animated spinner to non-interactive verbose mode ([#2001](#2001)) ([153f465](153f465)) * Add async backend support to local context middleware ([#2118](#2118)) ([a0d623c](a0d623c)) * Agent-friendly ux for scripted/headless workflows ([#2271](#2271)) ([386438f](386438f)) * AgentCore Code Interpreter sandbox provider ([#2120](#2120)) ([92556c7](92556c7)) * Context-aware connecting banner for resume and local server ([#2092](#2092)) ([18b385b](18b385b)) * Default langsmith project to `'deepagents-cli'` ([#2277](#2277)) ([7178b87](7178b87)) * Enhance tool-call UI, add `Ctrl+U` shortcut for chat input ([#1757](#1757)) ([800c552](800c552)) * Persist token count in graph state across sessions ([#2323](#2323)) ([5be352d](5be352d)) * Pop queued messages individually on `esc` instead of clearing all ([#2089](#2089)) ([c76d855](c76d855)) * Render ask-user questions as markdown ([#2339](#2339)) ([5fbb14a](5fbb14a)) * Show platform-specific ripgrep install command in missing-tool warning ([#1997](#1997)) ([f000ce5](f000ce5)) * Support root/MDM installs ([#2346](#2346)) ([f618acc](f618acc)) * Surface unsupported input modalities in system prompt ([#2327](#2327)) ([95620e7](95620e7)) ### Bug Fixes * Align headless todo guidance with non-interactive mode ([#2459](#2459)) ([281899b](281899b)) * Disable markup parsing for blocked-link notifications ([#2170](#2170)) ([15867bf](15867bf)) * Dismiss slash command autocomplete on space ([#2478](#2478)) ([02e46bc](02e46bc)) * Eliminate autocomplete popup flicker ([#2020](#2020)) ([4b2db1e](4b2db1e)) * Eliminate trace fragmentation in non-interactive mode ([#2136](#2136)) ([9bddc52](9bddc52)) * Enforce approval toggle when launched with `-y` ([#2278](#2278)) ([28a32b7](28a32b7)) * Escape exception text in rich markup error output ([#2307](#2307)) ([42bccca](42bccca)) * Escape markup in toast notifications ([#2139](#2139)) ([90ccc28](90ccc28)) * Exit app on `ctrl+d` when thread list is empty ([#2270](#2270)) ([e859077](e859077)) * Guard against textual cursor/document desync crash ([#2494](#2494)) ([c14a748](c14a748)) * Human-readable duration and consistent dim styling on teardown screen ([#1995](#1995)) ([901a0a4](901a0a4)) * Mark token count as approximate after interrupted generation ([#2353](#2353)) ([cb9a0c7](cb9a0c7)) * Resolve misleading "missing package" error when provider import fails ([#1960](#1960)) ([b90fbad](b90fbad)) * Open trace in browser immediately when busy ([#2305](#2305)) ([b452032](b452032)) * Patch model identity in system prompt on `/model` swap ([#2024](#2024)) ([36aecbf](36aecbf)) * Harden MCP pre-flight health checks ([#2019](#2019)) ([2b27055](2b27055)) * Pre-flight health checks for MCP servers ([#2008](#2008)) ([30d60e3](30d60e3)) * Prevent premature thinking state with parallel subtasks ([#1858](#1858)) ([189104c](189104c)) * Prevent session stats loss on mid-turn exit ([#2238](#2238)) ([b1807aa](b1807aa)) * Rebind toggle tool output to `ctrl+o` to unblock `cmd+right` ([#2088](#2088)) ([b486fe5](b486fe5)) * Remove duplicate server failure notification ([#2141](#2141)) ([c1cfe72](c1cfe72)) * Remove keybinding overrides that shadow textual built-ins ([#2084](#2084)) ([08fc5d0](08fc5d0)) * Resolve conflicting langsmith env var precedence ([#2455](#2455)) ([b6997d8](b6997d8)) * Show server startup error instead of generic agent message ([#2397](#2397)) ([a3e1e93](a3e1e93)) * Certain slash commands should not require server connection ([#1974](#1974)) ([32bd814](32bd814)) * Sort MCP tools deterministically for prompt-cache stability ([#2497](#2497)) ([39b43cf](39b43cf)) * Squash loading widget timer leak ([#2396](#2396)) ([01d3d86](01d3d86)) * Support pre-release versions in update checker ([#2164](#2164)) ([e18e9dc](e18e9dc)) * Surface clear error for missing sandbox provider deps ([#1999](#1999)) ([939f56a](939f56a)) * Use relative paths in langgraph config for Windows compat ([#2244](#2244)) ([d10dfbd](d10dfbd)) * Warn agent that local filesystem is inaccessible in sandbox mode ([#2274](#2274)) ([a3b61e5](a3b61e5)) * Wire `enable_ask_user` flag to remove tool in non-interactive mode ([#2105](#2105)) ([2399747](2399747)) ### Performance Improvements * Sub 250ms first paint ([#2027](#2027)) ([e42e05c](e42e05c)) * Defer `/model` selector data loading off event loop ([#2259](#2259)) ([a32ce7f](a32ce7f)) * Defer heavy imports from startup path ([#2022](#2022)) ([b7f5a99](b7f5a99)) * Defer pydantic and adapter imports out of startup hot path ([#2269](#2269)) ([0a410b4](0a410b4)) * Prewarm markdown stack and cache skill body render ([#2236](#2236)) ([0a3ba47](0a3ba47)) * Reduce health poll interval for local langgraph dev server ([#2283](#2283)) ([7f5c3de](7f5c3de)) --- _Everything above this line will be the GitHub release body._ --- > [!NOTE] > A **New Contributors** section is appended to the GitHub release notes automatically at publish time (see [Release Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline), step 2). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Mason Daugherty <[email protected]>
Non-interactive mode with a restrictive
--shell-allow-listused the HITL interrupt/resume path to vet shell commands. That made the CLI do the right shell gating, but every allowed shell command still paused the graph and resumed it withCommand(resume=...). In LangSmith, that split one CLI task into multiple trace runs under the same thread.What changed
DA_SERVER_toDEEPAGENTS_CLI_SERVER_ShellAllowListMiddlewareto validate shell commands inline and return an errorToolMessagefor rejected commands instead of pausing the graphgeneral-purposesubagent when this mode is active so the SDK's default doesn't bypass the allow-listBehavior
Before:
--shell-allow-list recommendedgated shell commands through HITLAfter:
interrupt()/Command(resume=...)ToolMessageimmediately