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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions libs/code/THREAT_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@
| T10 | DF24 | DC1 | MCP stdio subprocess env dict accepts arbitrary keys including `PATH`, `LD_PRELOAD`, `PYTHONPATH` without filtering | TB4 | Low | Verified | `mcp_tools._validate_server_config`, `mcp_tools._load_tools_from_config` |
| T12 | DF10 | — | Project `.env` sets shell startup-hook variables (`BASH_ENV`, `ENV`) that run attacker-controlled scripts when `dcode` spawns Bash, before any HITL approval | TB11 | High | Verified | `config._load_dotenv`, `local_context.build_detect_script` |
| T13 | DF7 | — | Configured shell allow-list checks only the first token, so an allow-listed interpreter/wrapper (`python3`, `bash`, `env`, `xargs`, …) runs arbitrary code via its arguments without approval in non-interactive mode | TB2 | Medium | Verified | `config.is_shell_command_allowed`, `config.contains_dangerous_patterns` |
| T14 | DF7, DF9 | — | A weaker model configured for the Auto approval classifier reviews gated actions less reliably, including untrusted text carried in tool arguments and file content | TB2 | Low | Verified | `auto_mode.AutoModeHITLMiddleware._classifier_model`, `config.resolve_auto_classifier_model` |
| T14 | DF7, DF9 | — | A weaker model configured for the Auto approval classifier reviews gated actions less reliably, including untrusted text carried in tool arguments and file content | TB2 | Low | Verified | `auto_mode.AutoModeHITLMiddleware._classifier_model`, `config.resolve_auto_classifier_model`, `config_manifest.resolve_auto_classifier_timeout` |

### Threat Details

Expand Down Expand Up @@ -442,7 +442,7 @@
#### T14: Weaker Auto Classifier Model Weakens Action Review

- **Flow**: DF7 (LLM tool call) → Auto classifier review → C4 Tools
- **Description**: In Auto approval mode, gated tool calls that deterministic policy cannot clear are reviewed by an LLM authorization classifier. That classifier can be pointed at a separate model (`--auto-classifier-model`, `DEEPAGENTS_CODE_AUTO_CLASSIFIER_MODEL`, `[models].auto_classifier`, or `/auto model`) so reviews are cheaper and faster than the main agent model. Review quality then follows the chosen model: a weaker one is likelier to mis-authorize an action, and likelier to be steered by injected instructions in the untrusted material it reads (tool arguments, paths, prior tool output, remote metadata). Choosing the classifier is a user-level decision, so it is restricted to trusted surfaces: shell exports, the global `~/.deepagents/.env`, `~/.deepagents/config.toml`, the CLI flag, and `/auto model`. A *project* `.env` travels with a cloned repo, so `DEEPAGENTS_CODE_AUTO_CLASSIFIER_MODEL` is listed in `config._PROJECT_DOTENV_DENIED_ENV_KEYS` — the same mitigation TB4 applies to the project-MCP trust vars; without that entry a checked-in `.env` could silently downgrade the review on `dcode` startup. With it, this is a self-inflicted weakening of a control rather than an external attack path. Auto's model-independent guards are unchanged by the setting: deterministic allow/deny, the consecutive/total denial counters, batch-replay detection, control-state availability, and the human-fallback thresholds. A classifier model that cannot be constructed (bad spec, missing credentials, uninstalled provider package) never falls back to the main model — the first such batch is marked `classifier_unavailable`, so those calls are denied and do not execute, and the failing spec is latched so every subsequent batch escalates straight to human approval until a review succeeds. A transient failure instead escalates once `_CONSECUTIVE_UNAVAILABLE_FALLBACK` consecutive batches have failed, or immediately when Auto's control state cannot be persisted.
- **Description**: In Auto approval mode, gated tool calls that deterministic policy cannot clear are reviewed by an LLM authorization classifier. That classifier can be pointed at a separate model (`--auto-classifier-model`, `DEEPAGENTS_CODE_AUTO_CLASSIFIER_MODEL`, `[models].auto_classifier`, or `/auto model`) so reviews are cheaper and faster than the main agent model. Review quality then follows the chosen model: a weaker one is likelier to mis-authorize an action, and likelier to be steered by injected instructions in the untrusted material it reads (tool arguments, paths, prior tool output, remote metadata). Choosing the classifier is a user-level decision, so it is restricted to trusted surfaces: shell exports, the global `~/.deepagents/.env`, `~/.deepagents/config.toml`, the CLI flag, and `/auto model`. A *project* `.env` travels with a cloned repo, so `DEEPAGENTS_CODE_AUTO_CLASSIFIER_MODEL` is listed in `config._PROJECT_DOTENV_DENIED_ENV_KEYS` — the same mitigation TB4 applies to the project-MCP trust vars; without that entry a checked-in `.env` could silently downgrade the review on `dcode` startup. With it, this is a self-inflicted weakening of a control rather than an external attack path. Auto's model-independent guards are unchanged by the setting: deterministic allow/deny, the consecutive/total denial counters, batch-replay detection, control-state availability, and the human-fallback thresholds. A classifier model that cannot be constructed (bad spec, missing credentials, uninstalled provider package) never falls back to the main model — the first such batch is marked `classifier_unavailable`, so those calls are denied and do not execute, and the failing spec is latched so every subsequent batch escalates straight to human approval until a review succeeds. A transient failure instead escalates once `_CONSECUTIVE_UNAVAILABLE_FALLBACK` consecutive batches have failed, or immediately when Auto's control state cannot be persisted. The review deadline is tunable through a subset of the same trusted surfaces — shell exports, the global `~/.deepagents/.env`, and `[models].auto_classifier_timeout` in `~/.deepagents/config.toml` (there is no CLI flag and no `/auto` subcommand for it) — and `DEEPAGENTS_CODE_AUTO_CLASSIFIER_TIMEOUT` is denied from a project `.env` for the same reason, so a cloned repo cannot stall gated batches or squeeze the budget until reviews time out. `config_manifest.resolve_auto_classifier_timeout` rejects any resolved value outside `[AUTO_CLASSIFIER_TIMEOUT_FLOOR, AUTO_CLASSIFIER_TIMEOUT_CEILING]` — out-of-range and malformed values are discarded in favor of the next config source rather than clamped — so the deadline itself cannot be removed; a timed-out batch remains fail-closed (`classifier_unavailable`).
- **Preconditions**: (1) Auto approval mode is active (interactive TUI only, and not under a sandbox); (2) a classifier model is configured through one of the trusted surfaces above — the default reuses the main agent model; (3) for the injected-review variant, untrusted content reaches the classifier through DF8/DF9 and the chosen model follows it. The intended control is that the human remains the fallback for anything the classifier does not affirmatively allow.

---
Expand Down Expand Up @@ -520,3 +520,4 @@ Threats that appear valid in isolation but fall outside project responsibility b
| 2026-07-24 | manual update | Corrected the out-of-scope hooks row: Hooks v2 defaults are 600s (30s for `UserPromptSubmit`), not a global 5-second timeout; loading is `hooks.loading.load_hooks_config`; project hooks require workspace trust or `--trust-project-hooks` |
| 2026-07-28 | manual update | Extended the out-of-scope hooks row for plugin-contributed hooks: enabled plugins may supply `hooks/hooks.json`, gated by install plus enablement rather than workspace trust, with each handler's environment overlaid only by its own plugin path variables |
| 2026-08-03 | manual update | Added T14 (a weaker Auto classifier model weakens action review) under TB2, covering the selectable classifier (`--auto-classifier-model`, `DEEPAGENTS_CODE_AUTO_CLASSIFIER_MODEL`, `[models].auto_classifier`, `/auto model`), its restriction to trusted config surfaces via `config._PROJECT_DOTENV_DENIED_ENV_KEYS`, and its fail-closed construction behavior (deny, then latch to human approval; never fall back to the main model). Extended the "LLM output" and "Configuration" input-coverage rows with T14 |
| 2026-08-04 | manual update | Extended T14 for the configurable Auto classifier review deadline (`DEEPAGENTS_CODE_AUTO_CLASSIFIER_TIMEOUT`, `[models].auto_classifier_timeout`): bounded by `config_manifest.resolve_auto_classifier_timeout` between a floor and ceiling so the deadline cannot be removed, denied from a project `.env` via `config._PROJECT_DOTENV_DENIED_ENV_KEYS`, and fail-closed on expiry |
17 changes: 17 additions & 0 deletions libs/code/deepagents_code/_env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@
can.
"""

AUTO_CLASSIFIER_TIMEOUT = "DEEPAGENTS_CODE_AUTO_CLASSIFIER_TIMEOUT"
"""Seconds the Auto approval-mode classifier may take to review one batch.

Raise this when reviews time out on a slow or heavily loaded classifier model:
a batch that misses the deadline is denied as `classifier_unavailable`, so the
tool call does not run and repeated misses escalate to your approval. This
covers the wait for a verdict only — the separate budget for *building* the
classifier model (cold provider import, credential bootstrap), which denies with
"could not be built within 30s", is fixed. Values outside 1-300 seconds are
ignored in favor of the next config source, so the deadline can never be
removed. Also settable via `[models].auto_classifier_timeout` in config.toml.
Resolved once per `dcode` start, so a change takes effect on the next launch.

Like `AUTO_CLASSIFIER_MODEL`, a committed *project* `.env` cannot set it (see
`config._PROJECT_DOTENV_DENIED_ENV_KEYS`).
"""

AUTO_UPDATE = "DEEPAGENTS_CODE_AUTO_UPDATE"
"""Toggle automatic app updates. Enabled by default; set to a falsy value
('0', 'false', 'no', 'off', or empty) to opt out."""
Expand Down
2 changes: 2 additions & 0 deletions libs/code/deepagents_code/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -2856,6 +2856,7 @@ def _subagent_cli_middleware(
if auto_mode_config is not None and resolved_interrupt_on is not None:
from deepagents_code.auto_mode import AutoModeHITLMiddleware
from deepagents_code.config import resolve_auto_classifier_model
from deepagents_code.config_manifest import resolve_auto_classifier_timeout

trusted_root, narrow_allow_list = auto_mode_config
# An explicit argument wins; otherwise the env var / `config.toml`
Expand All @@ -2872,6 +2873,7 @@ def _subagent_cli_middleware(
worktree_root=trusted_root,
shell_allow_list=narrow_allow_list,
classifier_model=classifier_model,
classifier_timeout_seconds=resolve_auto_classifier_timeout(),
trusted_ask_user_tool=trusted_ask_user_tool,
trusted_compaction_tool=compaction_middleware.tools[0],
)
Expand Down
20 changes: 19 additions & 1 deletion libs/code/deepagents_code/auto_mode.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import inspect
import json
import logging
import math
import os
import re
import shlex
Expand Down Expand Up @@ -64,6 +65,7 @@
aread_approval_mode_from_store,
coerce_approval_mode,
)
from deepagents_code.config_manifest import AUTO_CLASSIFIER_TIMEOUT_SECONDS_DEFAULT
from deepagents_code.goal_state_notice import project_goal_state

if TYPE_CHECKING:
Expand All @@ -78,7 +80,7 @@
)
USER_PROMPT_METADATA_KEY = "deepagents_code_user_prompt"
AUTO_MODE_EVENT_TYPE = "auto_mode"
_CLASSIFIER_TIMEOUT_SECONDS = 20.0
_CLASSIFIER_TIMEOUT_SECONDS = AUTO_CLASSIFIER_TIMEOUT_SECONDS_DEFAULT
# Building a classifier is a different kind of wait than asking one for a
# verdict: a cold provider-package import, profile resolution, and credential
# bootstrap all land on the first review. Sharing one budget made that first
Expand Down Expand Up @@ -1848,6 +1850,22 @@ def __init__(
):
msg = "trusted_compaction_tool must be named compact_conversation"
raise ValueError(msg)
# The review deadline is a security control's budget, so reject a
# nonsensical one at the boundary rather than trusting every caller:
# a zero, negative, or NaN timeout expires immediately, silently turning
# Auto into "deny every gated batch, then escalate". Callers that read
# user config go through `resolve_auto_classifier_timeout`, which bounds
# the value; this guards programmatic construction.
for name, budget in (
("classifier_timeout_seconds", classifier_timeout_seconds),
(
"classifier_construction_timeout_seconds",
classifier_construction_timeout_seconds,
),
):
if not math.isfinite(budget) or budget <= 0:
msg = f"{name} must be a positive finite number, got {budget!r}"
raise ValueError(msg)
interrupt_map = dict(interrupt_on)
interrupt_map["create_temp_artifact"] = {
"allowed_decisions": ["approve", "reject"],
Expand Down
14 changes: 13 additions & 1 deletion libs/code/deepagents_code/client/commands/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,10 @@ def _resolve(
`(is_set, source, value)`, where `is_set` is `False` when the value
came from the typed default.
"""
from deepagents_code.config_manifest import resolve_scalar
from deepagents_code.config_manifest import (
resolve_auto_classifier_timeout_with_source,
resolve_scalar,
)
from deepagents_code.model_config import ProviderAuthSource

if (
Expand All @@ -244,6 +247,15 @@ def _resolve(
if key is not None:
return True, ProviderAuthSource.STORED.value, key

if option.key == "models.auto_classifier_timeout":
# `resolve_scalar` alone would credit an out-of-range env value that the
# runtime rejects; use the bounded resolver so the display matches what
# the middleware actually enforces.
timeout, source = resolve_auto_classifier_timeout_with_source(
toml_data=toml_data
)
return source != "default", source, timeout

value, source = resolve_scalar(option, toml_data=toml_data)
return source != "default", source, value

Expand Down
17 changes: 13 additions & 4 deletions libs/code/deepagents_code/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from deepagents_code._constants import FIREWORKS_PROVIDER_ID_PREFIX
from deepagents_code._env_vars import (
AUTO_CLASSIFIER_MODEL,
AUTO_CLASSIFIER_TIMEOUT,
DANGEROUSLY_ENABLE_PROJECT_MCP_SERVERS,
DISABLED_PROJECT_MCP_SERVERS,
HIDE_SPLASH_VERSION,
Expand Down Expand Up @@ -173,6 +174,7 @@ class _BootstrapState:
DANGEROUSLY_ENABLE_PROJECT_MCP_SERVERS,
DISABLED_PROJECT_MCP_SERVERS,
AUTO_CLASSIFIER_MODEL,
AUTO_CLASSIFIER_TIMEOUT,
}
)
"""Env keys a *project* `.env` must not inject, even though they are otherwise
Expand All @@ -196,6 +198,13 @@ class _BootstrapState:
shell exports, the global `~/.deepagents/.env`, `[models].auto_classifier` in
`~/.deepagents/config.toml`, `--auto-classifier-model`, and `/auto model`.

`AUTO_CLASSIFIER_TIMEOUT` tunes the same control's review deadline, so it is
denied for the same reason: a cloned repo could otherwise stall every gated
batch up to the ceiling, or squeeze the budget until reviews time out and the
session degrades into repeated denials and approval prompts.
`[models].auto_classifier_timeout` in
`~/.deepagents/config.toml` and the trusted env surfaces still set it.

Unlike `_DOTENV_DENIED_ENV_KEYS` (denied from *any* `.env` because they turn
`.env` loading into code execution), these are denied only from the *project*
`.env`: the user's own global `~/.deepagents/.env` and their shell exports are
Expand Down Expand Up @@ -273,7 +282,7 @@ def apply_dotenv(dotenv_path: Path | None, *, is_project: bool) -> None:
if is_project and key in _PROJECT_DOTENV_DENIED_ENV_KEYS:
# Mirror `_load_dotenv`: a project `.env` cannot preview-set a
# user-level trust decision — MCP trust lists or the Auto
# classifier model (the global `.env`/shell can).
# classifier model/deadline (the global `.env`/shell can).
logger.debug(
"Ignoring project-denied env key %r from %s", key, dotenv_path
)
Expand Down Expand Up @@ -381,9 +390,9 @@ def apply_dotenv(dotenv_path: Path, *, is_project: bool) -> bool:
continue
if is_project and key in _PROJECT_DOTENV_DENIED_ENV_KEYS:
# A committed project `.env` must not set a user-level trust
# decision — MCP trust lists or the Auto classifier model that
# authorizes this repo's own tool calls; the global `.env` and
# shell may (is_project=False).
# decision — MCP trust lists or the Auto classifier model and
# deadline that authorize this repo's own tool calls; the
# global `.env` and shell may (is_project=False).
logger.debug(
"Ignoring project-denied env key %r from %s", key, dotenv_path
)
Expand Down
Loading