Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0e9c30a
add --allow-fs-tools to dcode
imnishitha Jul 10, 2026
14d0a58
tests
imnishitha Jul 10, 2026
cd766e4
Merge branch 'main' into nm/dcode-tools-allowlist
imnishitha Jul 10, 2026
3d70ef0
lint fixes
imnishitha Jul 10, 2026
20dd6cc
Merge branch 'main' into nm/dcode-tools-allowlist
mdrxy Jul 13, 2026
e8049d4
cr
mdrxy Jul 13, 2026
5bf26fe
cr
mdrxy Jul 13, 2026
9b45783
cr
mdrxy Jul 13, 2026
67f8972
cr
mdrxy Jul 13, 2026
02f6eae
Merge remote-tracking branch 'origin/main' into nm/dcode-tools-allowlist
mdrxy Jul 17, 2026
69e33ac
Merge branch 'main' into nm/dcode-tools-allowlist
mdrxy Jul 21, 2026
ccaab2c
feat(code): support a configurable tools allowlist
mdrxy Jul 21, 2026
c9c5659
Merge branch 'main' into nm/dcode-tools-allowlist
mdrxy Jul 21, 2026
7adece0
Merge remote-tracking branch 'origin/main' into nm/dcode-tools-allowlist
mdrxy Jul 21, 2026
d54fe15
Merge branch 'main' into nm/dcode-tools-allowlist
mdrxy Jul 21, 2026
76508ca
Merge branch 'main' into nm/dcode-tools-allowlist
mdrxy Jul 21, 2026
072483f
nits
mdrxy Jul 21, 2026
52642ba
refactor(code): remove duplicate filesystem tool guidance
mdrxy Jul 21, 2026
3e43e74
fix(code): extend `--allow-fs-tools` restriction to goal-criteria age…
mdrxy Jul 22, 2026
ad90d73
Merge branch 'main' into nm/dcode-tools-allowlist
mdrxy Jul 22, 2026
fdf6fbc
nits
mdrxy Jul 22, 2026
21b3246
fix(code): preserve SDK filesystem defaults for `all`
mdrxy Jul 22, 2026
af330c1
Merge branch 'main' into nm/dcode-tools-allowlist
mdrxy Jul 22, 2026
c7011c6
fix(code): hide guidance for disabled filesystem tools
mdrxy Jul 22, 2026
0b96152
Merge branch 'main' into nm/dcode-tools-allowlist
mdrxy Jul 22, 2026
0afe376
fix(code): validate filesystem tool allowlists
mdrxy Jul 22, 2026
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
14 changes: 14 additions & 0 deletions libs/code/deepagents_code/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@
DEFAULT_AGENT_NAME: Final[str] = "agent"
"""Default agent / assistant identifier when no `-a` flag is given."""

FS_TOOL_NAMES: Final[frozenset[str]] = frozenset(
{"ls", "read_file", "write_file", "edit_file", "delete", "glob", "grep", "execute"}
)
"""Mirror of the SDK's `FsToolName` literal members.

Hardcoded here rather than derived from `deepagents.FsToolName` because
`deepagents` must not be imported on the arg-parsing hot path (see AGENTS.md
"Startup performance"); this module is dependency-free and safe for `main.py` to
import. Consumers (`main._parse_allow_fs_tools_flag`,
`tool_catalog.collect_built_in_tools`) alias this set, and `get_args(FsToolName)`
drift guards in `test_main_args` and `test_tool_catalog` pin it so a new or
renamed SDK filesystem tool fails a test instead of silently diverging.
"""

FIREWORKS_PROVIDER_ID_PREFIX: Final[str] = "accounts/fireworks/"
"""Prefix used to infer Fireworks from fully-qualified IDs."""

Expand Down
100 changes: 98 additions & 2 deletions libs/code/deepagents_code/_server_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
import os
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast

from deepagents_code._constants import DEFAULT_AGENT_NAME as DEFAULT_ASSISTANT_ID
from deepagents_code._env_vars import SERVER_ENV_PREFIX

if TYPE_CHECKING:
from deepagents import FsToolName

from deepagents_code.project_utils import ProjectContext


Expand Down Expand Up @@ -68,6 +70,63 @@ def _read_env_json(suffix: str) -> Any: # noqa: ANN401
raise ValueError(msg) from exc


def _read_env_allow_fs_tools() -> list[FsToolName] | None:
"""Read and shape-validate the `ALLOW_FS_TOOLS` filesystem allowlist.

The parent writes only an absent variable (unrestricted — `None`, which is
also what `--allow-fs-tools all` collapses to) or a non-empty JSON list of
tool names (`main._parse_allow_fs_tools_flag`). This runs in the server
subprocess, where the variable could be tampered with, so — because the
value is a security control — any unrecognized shape must fail closed
(raise) rather than fall through to an unrestricted filesystem.
(`_read_env_json` already fails closed on malformed JSON.)

`[]` and unknown tool names are rejected here, not deferred downstream, so
the returned list genuinely satisfies `list[FsToolName]` and the `cast`
asserts membership that was actually checked. Importing `deepagents` here is
fine: the subprocess already imports the SDK to build the agent (this is not
the arg-parsing hot path guarded in `main`). The `"read_file"` requirement
is not checked here: `ServerConfig.__post_init__` enforces it when the
returned value is placed on the config (with `FilesystemMiddleware` as a
final backstop), so a tampered list without `read_file` still fails closed
at construction.

Returns:
`None` when the variable is absent, or a non-empty list of filesystem
tool-name strings, each a valid `FsToolName`.

Raises:
ValueError: If the present variable parses to anything other than a
non-empty list of strings, or if any list element is not a
recognized filesystem tool name.
"""
env_name = f"{SERVER_ENV_PREFIX}ALLOW_FS_TOOLS"
if env_name not in os.environ:
return None

raw = _read_env_json("ALLOW_FS_TOOLS")
if isinstance(raw, list) and raw and all(isinstance(name, str) for name in raw):
from typing import get_args

from deepagents import FsToolName

valid_names = frozenset(get_args(FsToolName))
unknown = [name for name in raw if name not in valid_names]
if unknown:
msg = (
f"Invalid {SERVER_ENV_PREFIX}ALLOW_FS_TOOLS value: unknown "
f"filesystem tool name(s) {unknown!r}; valid names are "
f"{sorted(valid_names)}."
)
raise ValueError(msg)
return cast("list[FsToolName]", raw)
msg = (
f"Invalid {SERVER_ENV_PREFIX}ALLOW_FS_TOOLS value: {raw!r}; expected "
"a non-empty list of filesystem tool names."
)
raise ValueError(msg)


def _read_env_str(suffix: str) -> str | None:
"""Read an optional `DEEPAGENTS_CODE_SERVER_*` string variable.

Expand Down Expand Up @@ -261,6 +320,17 @@ class ServerConfig:
`interpreter_ptc="all"` is paired with non-`auto_approve` mode.
"""

allow_fs_tools: list[FsToolName] | None = None
"""Allowlist for `FilesystemMiddleware`'s `tools` param, from
`--allow-fs-tools`.

`None` means "all filesystem tools" and is also what `--allow-fs-tools all`
parses to: it leaves the SDK's own default `FilesystemMiddleware` in place
(no replacement). A list is an explicit allowlist of filesystem tool names,
must include `"read_file"`, and installs a restricted replacement (see
`create_cli_agent`).
"""

rubric_model: str | None = None
"""Grader model spec for `RubricMiddleware` (e.g. `'anthropic:...'`).

Expand Down Expand Up @@ -307,14 +377,29 @@ def __post_init__(self) -> None:

Raises:
TypeError: If `rubric_max_iterations` is a boolean.
ValueError: If `shell_allow_list` is an empty list or
ValueError: If `shell_allow_list` is an empty list,
`allow_fs_tools` is an empty list or omits `"read_file"`, or
`rubric_max_iterations` is non-positive.
"""
if self.sandbox_type == "none":
object.__setattr__(self, "sandbox_type", None)
if self.shell_allow_list is not None and len(self.shell_allow_list) == 0:
msg = "shell_allow_list must be None or non-empty"
raise ValueError(msg)
# `allow_fs_tools` is a security control: `None` means unrestricted, but
# an explicit list must be a usable allowlist. Own the non-empty +
# `read_file`-required invariant here (the single authoritative point
# for both the env round-trip via `from_env` and direct construction)
# rather than deferring to `FilesystemMiddleware`, which would only
# surface the violation a process boundary away. `_parse_allow_fs_tools_flag`
# still enforces the same rule at the CLI for a friendlier error.
if self.allow_fs_tools is not None:
if len(self.allow_fs_tools) == 0:
msg = "allow_fs_tools must be None or a non-empty list"
raise ValueError(msg)
if "read_file" not in self.allow_fs_tools:
msg = "allow_fs_tools must include 'read_file'"
raise ValueError(msg)
if isinstance(self.rubric_max_iterations, bool):
msg = "rubric_max_iterations must be None or a positive integer"
raise TypeError(msg)
Expand Down Expand Up @@ -370,6 +455,11 @@ def to_env(self) -> dict[str, str | None]:
"INTERPRETER_PTC_ACKNOWLEDGE_UNSAFE": str(
self.interpreter_ptc_acknowledge_unsafe
).lower(),
"ALLOW_FS_TOOLS": (
json.dumps(self.allow_fs_tools)
if self.allow_fs_tools is not None
else None
),
"RUBRIC_MODEL": self.rubric_model,
"RUBRIC_MAX_ITERATIONS": (
str(self.rubric_max_iterations)
Expand Down Expand Up @@ -425,6 +515,7 @@ def from_env(cls) -> ServerConfig:
interpreter_ptc_acknowledge_unsafe=_read_env_bool(
"INTERPRETER_PTC_ACKNOWLEDGE_UNSAFE"
),
allow_fs_tools=_read_env_allow_fs_tools(),
rubric_model=_read_env_str("RUBRIC_MODEL") or None,
rubric_max_iterations=_read_env_int("RUBRIC_MAX_ITERATIONS", default=None),
sandbox_type=_read_env_str("SANDBOX_TYPE"),
Expand Down Expand Up @@ -463,6 +554,7 @@ def from_cli_args(
enable_interpreter: bool | None = None,
interpreter_ptc: str | list[str] | None = None,
interpreter_ptc_acknowledge_unsafe: bool = False,
allow_fs_tools: list[FsToolName] | None = None,
rubric_model: str | None = None,
rubric_max_iterations: int | None = None,
mcp_config_path: str | None,
Expand Down Expand Up @@ -499,6 +591,9 @@ def from_cli_args(
interpreter_ptc: Override for `settings.interpreter_ptc`.
interpreter_ptc_acknowledge_unsafe: Mirror of
`settings.interpreter_ptc_acknowledge_unsafe`.
allow_fs_tools: Allowlist for `FilesystemMiddleware`'s `tools`
param to forward to the server subprocess. `None` leaves the
SDK default (all tools).
rubric_model: Grader model spec; `None` reuses the main model.
rubric_max_iterations: Explicit grader iterations per rubric attempt;
`None` uses the SDK default.
Expand Down Expand Up @@ -530,6 +625,7 @@ def from_cli_args(
enable_interpreter=resolved_enable_interpreter,
interpreter_ptc=interpreter_ptc,
interpreter_ptc_acknowledge_unsafe=interpreter_ptc_acknowledge_unsafe,
allow_fs_tools=allow_fs_tools,
rubric_model=rubric_model,
rubric_max_iterations=rubric_max_iterations,
sandbox_type=sandbox_type,
Expand Down
Loading