fix(code): exclude managed bin dir from agent picker (#4190)

Fix the `/agents` picker listing the internal `bin` directory (managed
ripgrep install) as a selectable agent.

---

The `/agents` picker lists every real, non-dot subdirectory under
`~/.deepagents/`, filtering only symlinks and dot-prefixed names. The
managed ripgrep install dir (`~/.deepagents/bin`,
`managed_tools.BIN_DIR`) therefore showed up as a phantom `bin` agent.
This adds a reserved-name filter to `_is_agent_dir_entry`, sourced from
`BIN_DIR.name` to keep a single source of truth.

Made by [Open
SWE](https://openswe.vercel.app/agents/3cd7dda7-db49-2afd-b636-d678ecc65dc0)

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-06-23 18:31:40 -04:00
committed by GitHub
parent afbc56a1d9
commit d869d1e1fa
2 changed files with 44 additions and 6 deletions
+23 -6
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import functools
import logging
import os
import re
@@ -438,17 +439,32 @@ def load_async_subagents(config_path: Path | None = None) -> list[AsyncSubAgent]
return agents
@functools.lru_cache(maxsize=1)
def _reserved_agent_dir_names() -> frozenset[str]:
"""Return non-agent directory names reserved by the app under `~/.deepagents/`.
`bin/` holds the managed `rg` binary (`managed_tools.BIN_DIR`) and must
never appear in the agent picker. The name is derived from `BIN_DIR` so it
stays a single source of truth rather than being hardcoded here. The result
is cached since the reserved set is constant for the process.
"""
from deepagents_code.managed_tools import BIN_DIR
return frozenset({BIN_DIR.name})
def _is_agent_dir_entry(entry: Path) -> bool:
"""Return whether a `~/.deepagents/` entry should be listed as an agent.
Filters out symlinks (so dangling links don't masquerade as agents)
and dot-prefixed names — `.state/` (app internal state) plus any
other hidden directory the user may have placed there.
Filters out symlinks (so dangling links don't masquerade as agents),
dot-prefixed names — `.state/` (app internal state) plus any other
hidden directory the user may have placed there — and reserved names
the app owns (e.g. `bin/`, the managed-binary install dir).
`OSError` from `is_dir`/`is_symlink` propagates so callers can log
with the failing entry's name as context.
"""
if entry.name.startswith("."):
if entry.name.startswith(".") or entry.name in _reserved_agent_dir_names():
return False
return entry.is_dir() and not entry.is_symlink()
@@ -458,8 +474,9 @@ def get_available_agent_names() -> list[str]:
Scans the user's `.deepagents` directory and returns each real
subdirectory found there. Symlinks excluded so a dangling link does not
masquerade as an agent. Dot-prefixed entries (e.g., `.state/`) are
skipped so internal app state never appears as an agent.
masquerade as an agent. Dot-prefixed entries (e.g., `.state/`) and
reserved app-owned directories (e.g., `bin/`, the managed-binary install
dir) are skipped so internal state never appears as an agent.
Filesystem errors (missing parent, permission denied, broken entries) are
logged and surfaced as an empty list rather than raised — the caller shows
+21
View File
@@ -30,6 +30,7 @@ from deepagents_code.agent import (
_format_task_description,
_format_web_search_description,
_format_write_file_description,
_reserved_agent_dir_names,
_sanitize_agent_message_name,
_should_interrupt_tool_call,
build_model_identity_section,
@@ -40,6 +41,7 @@ from deepagents_code.agent import (
load_async_subagents,
)
from deepagents_code.config import Settings, get_glyphs
from deepagents_code.managed_tools import BIN_DIR
from deepagents_code.project_utils import ProjectContext
@@ -2933,6 +2935,25 @@ class TestGetAvailableAgentNames:
with patch("deepagents_code.agent.settings", _mock_agents_dir(agents_dir)):
assert get_available_agent_names() == ["agent"]
def test_ignores_reserved_bin_dir(self, tmp_path: Path) -> None:
"""The managed-binary install dir is excluded from the agent list.
The reserved dir name is derived from `BIN_DIR.name` rather than the
literal `bin` so a future rename of `BIN_DIR` keeps this test exercising
the actual reserved name instead of a stale string.
"""
agents_dir = tmp_path / "agents"
agents_dir.mkdir()
(agents_dir / "agent").mkdir()
(agents_dir / BIN_DIR.name).mkdir()
with patch("deepagents_code.agent.settings", _mock_agents_dir(agents_dir)):
assert get_available_agent_names() == ["agent"]
def test_reserved_agent_dir_names_includes_bin_dir(self) -> None:
"""The reserved-name set is sourced from `BIN_DIR.name` (single source)."""
assert _reserved_agent_dir_names() == frozenset({BIN_DIR.name})
def test_permission_error_returns_empty(self, tmp_path: Path) -> None:
"""PermissionError on iterdir → logged + empty list, not raised."""
agents_dir = tmp_path / "agents"