fix(sandbox): list the skills root when only category mounts exist by BlueX888 · Pull Request #5857 · bytedance/deer-flow · GitHub
Skip to content

fix(sandbox): list the skills root when only category mounts exist - #5857

Merged
WillemJiang merged 5 commits into
bytedance:mainfrom
BlueX888:fix/prep-local-sandbox-skills-root-unlistable
Sep 25, 2026
Merged

WillemJiang merged 5 commits into
bytedance:mainfrom
BlueX888:fix/prep-local-sandbox-skills-root-unlistable

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

Why

With the default (non-policy-scoped) skills projection, the agent's ls /mnt/skills returned Error: Directory not found: /mnt/skills even with skills installed and all four categories mounted.

Root cause: LocalSandboxProvider mounts the skills tree in one of two layouts (backend/packages/harness/deerflow/sandbox/local/local_sandbox_provider.py:369-406). When the projection is policy-scoped (per-user isolated), an aggregate PathMapping maps /mnt/skills itself to a real host directory, so listing works. In the default layout there is no mapping for the /mnt/skills root — only the four category mounts (/mnt/skills/public, custom, legacy, integrations), each pointing at its own host directory. LocalSandbox.list_dir then resolved /mnt/skills to the literal, unmapped host path (local_sandbox.py:771-772 — _resolve_path_with_mapping returns an unmatched path unchanged), and the host list_dir raised FileNotFoundError (list_dir.py:26-27) before the virtual sub-directory overlay — the block that exists exactly so the agent can discover the categories via ls /mnt/skills (local_sandbox.py:795-800) — could run. The ls tool surfaced that as "Directory not found" (backend/packages/harness/deerflow/sandbox/tools.py:2326-2327).

What changed

LocalSandbox.list_dir now treats a FileNotFoundError from the host scan as an empty host listing when at least one mapping is mounted inside the requested container path, letting the existing virtual sub-directory overlay surface those children — ls /mnt/skills lists the four category directories. Any path with nothing mounted inside it keeps raising FileNotFoundError exactly as before, so genuinely missing directories still error. No other list_dir behavior changes.

Surface area

  • Frontend UI — page / component / setting / interaction under frontend/
  • Backend API — endpoint / SSE event / request-response shape under backend/app
  • Agents / LangGraph — agent node, graph wiring, langgraph.json, or prompt change
    • Prompt-layer self-check: n/a — no prompt or data-channel change.
  • Sandbox — docker/ or sandboxed execution
  • Skills — change under skills/
  • Dependencies — new/upgraded entry in backend/pyproject.toml or frontend/package.json (say what it buys us)
  • Default behavior change — changes existing behavior without the user opting in (default model, default setting, data shape)
  • Docs / tests / CI only — no runtime behavior change

Screenshots / Recording

N/A — backend sandbox fix, no UI surface.

Bug fix verification

  • Test path that reproduces the bug: backend/tests/test_local_sandbox_virtual_path_contract.py::test_list_dir_on_skills_root_lists_category_mounts
  • Did it go red on main and green on this branch? yes

Red run on the unpatched tree (local_sandbox.py at origin/main, test kept):

>       entries = sbx.list_dir("/mnt/skills")
tests/test_local_sandbox_virtual_path_contract.py:188:
packages/harness/deerflow/sandbox/local/local_sandbox.py:773: in list_dir
    entries = list_dir(resolved_path, max_depth)
E           FileNotFoundError: /mnt/skills
packages/harness/deerflow/sandbox/local/list_dir.py:27: FileNotFoundError
FAILED tests/test_local_sandbox_virtual_path_contract.py::test_list_dir_on_skills_root_lists_category_mounts
============================== 1 failed in 0.77s ===============================

The test pins the scenario first — it asserts /mnt/skills itself is absent from sbx.path_mappings while all four category mounts are present — so it reproduces the category-only layout rather than failing incidentally. Green on this branch: 1 passed in 0.59s.

Validation

  • PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run pytest tests/test_local_sandbox_virtual_path_contract.py::test_list_dir_on_skills_root_lists_category_mounts -v → 1 passed on this branch; 1 failed (FileNotFoundError: /mnt/skills) on the unpatched tree
  • PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run pytest tests/test_local_sandbox_command_timeout.py tests/test_local_sandbox_encoding.py tests/test_local_sandbox_path_regex_cache.py tests/test_local_sandbox_provider_mounts.py tests/test_local_sandbox_virtual_path_contract.py tests/test_sandbox_search_contract.py tests/test_sandbox_tools_security.py -q → 408 passed, 11 skipped
  • make lint → ruff check All checks passed!, ruff format --check 1633 files already formatted
  • PYTHONPATH=. PYTHONIOENCODING=utf-8 PYTHONUTF8=1 uv run pytest tests/test_local_sandbox_virtual_path_contract.py -q → 25 passed

AI assistance

Tool(s) used: Claude Code

How you used it: The agent traced the ls /mnt/skills failure through the provider's two mount layouts and the list_dir overlay, and drafted the FileNotFoundError guard plus the regression test; the diff was reviewed line by line and the red→green run, module suites, and lint above were executed and verified.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

With the default (non-policy-scoped) LocalSandboxProvider layout, the
four skill categories (/mnt/skills/public, custom, legacy, integrations)
are mounted at separate host directories and there is no mapping for the
aggregate /mnt/skills root itself. LocalSandbox.list_dir resolved the
root to the unmapped host path, and the host list_dir raised
FileNotFoundError before the virtual sub-directory overlay -- which
exists exactly so the agent can discover the categories via
"ls /mnt/skills" -- could run, so the ls tool reported "Directory not
found: /mnt/skills" even with skills installed.

Treat a FileNotFoundError from the host scan as an empty listing when
mappings are mounted inside the requested container path, so the overlay
can surface those children. Paths with nothing mounted inside them keep
raising FileNotFoundError as before.
@github-actions github-actions Bot added area:sandbox Sandboxed execution and docker/ risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines labels Sep 25, 2026
@github-actions github-actions Bot added size/M PR changes 100-300 lines and removed size/S PR changes 20-100 lines labels Sep 25, 2026

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, focused fix. I verified the root cause against the head commit: the default (non-policy-scoped) provider layout really does mount only the four category paths under /mnt/skills with no aggregate root mapping, and _resolve_path_with_mapping leaves unmatched paths unchanged, so the host list_dir got the literal /mnt/skills and raised before the virtual-children overlay could run. The red->green regression test plus the two negative tests (nested-only mount, missing direct-child host dir) pin the contract well. Two minor notes below, neither blocking.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The list_dir utility raises FileNotFoundError for two distinct cases — path missing and path exists but is not a directory (if not root_path.is_dir(): raise FileNotFoundError(path) in list_dir.py) — so this guard also swallows the not-a-directory case: if a mapping root ever resolved to a host file while a direct child mount exists, the call would now silently return the virtual children instead of surfacing the misconfiguration. Since this branch only executes on the error path, a cheap disambiguation would keep that error visible, e.g. if not virtual_children or os.path.exists(resolved_path): raise. The PR comment ("the resolved host directory is missing") slightly overstates what the exception guarantees, so either the code or the comment could be tightened.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: self._resolved_local_paths[mapping] already caches os.path.realpath(mapping.local_path) per mapping — the class comment above notes the cached views exist precisely to avoid repeated filesystem resolution on the agent's hot path. This pre-scan now runs on every list_dir call, so it could reuse the cache: os.path.isdir(self._resolved_local_paths[mapping]) (same value, no extra realpath syscall per child mount per call).

@WillemJiang
WillemJiang merged commit f92118e into bytedance:main Sep 25, 2026
16 checks passed
@WillemJiang WillemJiang added this to the 2.2.0 milestone Sep 25, 2026
hawkli-1994 pushed a commit to rongxinzy/deer-flow that referenced this pull request Sep 27, 2026
…ytedance#5857)

* fix(sandbox): list the skills root when only category mounts exist

With the default (non-policy-scoped) LocalSandboxProvider layout, the
four skill categories (/mnt/skills/public, custom, legacy, integrations)
are mounted at separate host directories and there is no mapping for the
aggregate /mnt/skills root itself. LocalSandbox.list_dir resolved the
root to the unmapped host path, and the host list_dir raised
FileNotFoundError before the virtual sub-directory overlay -- which
exists exactly so the agent can discover the categories via
"ls /mnt/skills" -- could run, so the ls tool reported "Directory not
found: /mnt/skills" even with skills installed.

Treat a FileNotFoundError from the host scan as an empty listing when
mappings are mounted inside the requested container path, so the overlay
can surface those children. Paths with nothing mounted inside them keep
raising FileNotFoundError as before.

* fix(sandbox): preserve missing virtual parent errors

* test(screening): align PII fixture with keyed placeholders

* fix(sandbox): preserve non-directory errors for virtual parents

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:sandbox Sandboxed execution and docker/ risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/M PR changes 100-300 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants