feat(agent-server): add include_hidden to file browser endpoints#3740
Conversation
The folder/file browser endpoints (/api/file/search_subdirs and /api/file/home) unconditionally skip dot-entries, so GUIs built on them cannot browse or select hidden directories (e.g. ~/.config). Add an optional include_hidden query param (default False, so existing behavior is unchanged) to both endpoints. When true, dot-directories are listed; files and symlinks are still skipped. Co-authored-by: openhands <openhands@all-hands.dev>
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
all-hands-bot
left a comment
There was a problem hiding this comment.
⚠️ QA Report: PASS WITH ISSUES
Functional QA passed: the running agent-server now exposes hidden directories only when include_hidden=true, while default behavior still omits them.
Does this PR achieve its stated goal?
Yes. I started the actual agent-server and made real HTTP requests against /api/file/search_subdirs and /api/file/home before and after the PR. On origin/main, adding include_hidden=true still omitted dot-directories; on commit ad0f28e8, the same requests returned .hidden_repo and .config only with the opt-in flag, while files and symlinks remained excluded.
| Phase | Result |
|---|---|
| Environment Setup | ✅ make build completed and installed the uv workspace dependencies. |
| CI Status | Validate PR description failed. |
| Functional Verification | ✅ Real HTTP requests verified both changed endpoints and unchanged defaults. |
Functional Verification
Test 1: /api/file/search_subdirs hidden directory opt-in
Step 1 — Reproduce / establish baseline without the fix:
Checked out origin/main, created a real directory containing projects/, .hidden_repo/, README.md, and a symlink, then started the server with:
HOME=/tmp/file-browser-qa-base/home OPENHANDS_SUPPRESS_BANNER=1 uv run agent-server --host 127.0.0.1 --port 8765Ran:
curl 'http://127.0.0.1:8765/api/file/search_subdirs?path=%2Ftmp%2Ffile-browser-qa-base%2Fsearch'
curl 'http://127.0.0.1:8765/api/file/search_subdirs?path=%2Ftmp%2Ffile-browser-qa-base%2Fsearch&include_hidden=true'Observed both responses returned only the visible directory:
{
"items": [
{"name": "projects", "path": "/tmp/file-browser-qa-base/search/projects"}
],
"next_page_id": null
}This establishes the prior behavior: even when a client sent include_hidden=true, hidden directories were not listed.
Step 2 — Apply the PR's changes:
Checked out ad0f28e881ee8e6c8182bb4257f31f2370046357 and recreated the same fixture shape under /tmp/file-browser-qa-pr/search.
Step 3 — Re-run with the fix in place:
Ran the equivalent requests against the PR server. Default behavior still omitted hidden entries:
{
"items": [
{"name": "projects", "path": "/tmp/file-browser-qa-pr/search/projects"}
],
"next_page_id": null
}With include_hidden=true, the hidden directory appeared and the file/symlink stayed excluded:
{
"items": [
{"name": ".hidden_repo", "path": "/tmp/file-browser-qa-pr/search/.hidden_repo"},
{"name": "projects", "path": "/tmp/file-browser-qa-pr/search/projects"}
],
"next_page_id": null
}This confirms the new opt-in behavior works and the backward-compatible default is preserved.
Test 2: /api/file/home hidden favorite opt-in
Step 1 — Reproduce / establish baseline without the fix:
On origin/main, started the server with HOME=/tmp/file-browser-qa-base/home where the home directory contained Documents/, .config/, readme.txt, and a symlink.
Ran:
curl 'http://127.0.0.1:8765/api/file/home'
curl 'http://127.0.0.1:8765/api/file/home?include_hidden=true'Observed both responses returned only Documents in favorites:
{
"home": "/tmp/file-browser-qa-base/home",
"favorites": [
{"label": "Documents", "path": "/tmp/file-browser-qa-base/home/Documents"}
],
"locations": [{"label": "/", "path": "/"}]
}This confirms the old endpoint could not opt into hidden home favorites.
Step 2 — Apply the PR's changes:
Checked out ad0f28e881ee8e6c8182bb4257f31f2370046357 and recreated the same fixture shape under /tmp/file-browser-qa-pr/home.
Step 3 — Re-run with the fix in place:
Default behavior still returned only the visible favorite:
{
"home": "/tmp/file-browser-qa-pr/home",
"favorites": [
{"label": "Documents", "path": "/tmp/file-browser-qa-pr/home/Documents"}
],
"locations": [{"label": "/", "path": "/"}]
}With include_hidden=true, .config appeared before Documents, while the file and symlink stayed excluded:
{
"home": "/tmp/file-browser-qa-pr/home",
"favorites": [
{"label": ".config", "path": "/tmp/file-browser-qa-pr/home/.config"},
{"label": "Documents", "path": "/tmp/file-browser-qa-pr/home/Documents"}
],
"locations": [{"label": "/", "path": "/"}]
}This confirms /api/file/home now supports the same hidden-directory opt-in behavior as the search endpoint.
Issues Found
- 🟡 Non-functional CI issue:
Validate PR descriptionis failing with:A human must check A human has tested these changes.andAdd a short human-written note between HUMAN: and the human-tested checkbox.Per repo policy, a human author needs to update those human-only PR description fields.
No functional issues found in the changed behavior.
This QA review was created by an AI agent (OpenHands) on behalf of the user.
|
✅ Review complete. This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here. |
all-hands-bot
left a comment
There was a problem hiding this comment.
Taste: 🟢 Good taste — clean, minimal, backward-compatible feature addition.
Summary
This PR adds an optional include_hidden query parameter (default False) to GET /api/file/search_subdirs and GET /api/file/home, letting GUIs opt in to listing dot-directories (e.g. ~/.config, ~/.ssh) without changing the default for existing clients.
Why this is good
- Real problem, simple solution. The dot-skip was hard-coded; the fix is a single boolean flag that flips the existing condition. No new data structures, no new abstractions — the cleanest possible answer to the request.
- Backward compatible by construction. Default is
Falseon both endpoints, so no existing client changes behavior. The Python and OpenAPI breakage checks both passed in CI. - No new attack surface. The flag only relaxes an existing filter.
os.scandirstill operates on a server-validated absolute path;entry.is_dir(follow_symlinks=False)still rejects symlinks; absolute-path / exists / is-dir validation is unchanged. A client that wanted~/.sshcould already setpath=$HOMEand get the visible list — this just lets them also see the hidden ones. - Real tests. Two new tests use a real
tmp_pathfilesystem and the real FastAPITestClient, asserting on actual JSON output (not mocks). The default-false paths are already covered by the existing tests. - Coherent docstrings. Updated inline rather than left as stale documentation.
CI: all 30 checks green (build, test, OpenAPI breakage, Python API breakage, coverage, QA). The only "blocked" state is the missing human-tester checkbox in the PR template, which is a process gate, not a code issue.
Approving.
This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation
all-hands-bot
left a comment
There was a problem hiding this comment.
✅ QA Report: PASS
The file browser API now exposes hidden directories only when include_hidden=true, while preserving the default hidden-directory filtering behavior.
Does this PR achieve its stated goal?
Yes. I ran the real agent-server on the base commit and on the PR commit, then queried the affected HTTP endpoints with a home directory containing visible dirs, hidden dirs, hidden files, normal files, and a symlink. On base, include_hidden=true still omitted .config and .hidden_repo; on the PR commit, the same requests included hidden directories for both /api/file/search_subdirs and /api/file/home, while default requests still returned only projects and files/symlinks remained absent from directory results.
| Phase | Result |
|---|---|
| Environment Setup | ✅ make build completed and installed the uv workspace packages. |
| CI Status | Validate PR description was failing and this QA workflow was in progress. |
| Functional Verification | ✅ Real HTTP requests to both changed endpoints matched the PR description. |
Functional Verification
Test 1: /api/file/search_subdirs hidden-directory opt-in
Step 1 — Reproduce / establish baseline without the fix:
Checked out base ea4e0cc5, created a temporary home containing projects/, .config/, .hidden_repo/, .hidden_file, readme.txt, and a symlink to projects/, then started the real server:
HOME=/tmp/oh-file-browser-home.pLnqy8 uv run python -m openhands.agent_server --host 127.0.0.1 --port 8765Ran:
curl -sS "http://127.0.0.1:8765/api/file/search_subdirs?path=/tmp/oh-file-browser-home.pLnqy8&include_hidden=true" | jq '{items: [.items[] | {name, path}], next_page_id}'Output:
{
"items": [
{
"name": "projects",
"path": "/tmp/oh-file-browser-home.pLnqy8/projects"
}
],
"next_page_id": null
}This establishes the prior behavior: even with include_hidden=true, hidden directories were not visible to the file browser endpoint.
Step 2 — Apply the PR's changes:
Checked out PR commit ad0f28e8 and started the real server against the same temporary home:
HOME=/tmp/oh-file-browser-home.pLnqy8 uv run python -m openhands.agent_server --host 127.0.0.1 --port 8766Step 3 — Re-run with the fix in place:
Default request:
curl -sS "http://127.0.0.1:8766/api/file/search_subdirs?path=/tmp/oh-file-browser-home.pLnqy8" | jq '{items: [.items[] | {name, path}], next_page_id}'Output:
{
"items": [
{
"name": "projects",
"path": "/tmp/oh-file-browser-home.pLnqy8/projects"
}
],
"next_page_id": null
}Opt-in request:
curl -sS "http://127.0.0.1:8766/api/file/search_subdirs?path=/tmp/oh-file-browser-home.pLnqy8&include_hidden=true" | jq '{items: [.items[] | {name, path}], next_page_id}'Output:
{
"items": [
{
"name": ".config",
"path": "/tmp/oh-file-browser-home.pLnqy8/.config"
},
{
"name": ".hidden_repo",
"path": "/tmp/oh-file-browser-home.pLnqy8/.hidden_repo"
},
{
"name": "projects",
"path": "/tmp/oh-file-browser-home.pLnqy8/projects"
}
],
"next_page_id": null
}This confirms the new opt-in behavior works and the default behavior remains backward-compatible. The hidden file, normal file, and symlink were not returned.
Test 2: /api/file/home hidden favorites opt-in
Step 1 — Reproduce / establish baseline without the fix:
On base ea4e0cc5, with the same temporary home, ran:
curl -sS "http://127.0.0.1:8765/api/file/home?include_hidden=true" | jq '{home, favorites: [.favorites[] | {label, path}], locations}'Output:
{
"home": "/tmp/oh-file-browser-home.pLnqy8",
"favorites": [
{
"label": "projects",
"path": "/tmp/oh-file-browser-home.pLnqy8/projects"
}
],
"locations": [
{
"label": "/",
"path": "/"
}
]
}This establishes the old behavior for home favorites: hidden top-level directories were omitted even when the client attempted to request them.
Step 2 — Apply the PR's changes:
Checked out PR commit ad0f28e8 and used the same running PR server setup from Test 1.
Step 3 — Re-run with the fix in place:
Default request:
curl -sS "http://127.0.0.1:8766/api/file/home" | jq '{home, favorites: [.favorites[] | {label, path}], locations}'Output:
{
"home": "/tmp/oh-file-browser-home.pLnqy8",
"favorites": [
{
"label": "projects",
"path": "/tmp/oh-file-browser-home.pLnqy8/projects"
}
],
"locations": [
{
"label": "/",
"path": "/"
}
]
}Opt-in request:
curl -sS "http://127.0.0.1:8766/api/file/home?include_hidden=true" | jq '{home, favorites: [.favorites[] | {label, path}], locations}'Output:
{
"home": "/tmp/oh-file-browser-home.pLnqy8",
"favorites": [
{
"label": ".config",
"path": "/tmp/oh-file-browser-home.pLnqy8/.config"
},
{
"label": ".hidden_repo",
"path": "/tmp/oh-file-browser-home.pLnqy8/.hidden_repo"
},
{
"label": "projects",
"path": "/tmp/oh-file-browser-home.pLnqy8/projects"
}
],
"locations": [
{
"label": "/",
"path": "/"
}
]
}This confirms /api/file/home now supports the same hidden-directory opt-in behavior as search_subdirs, with default behavior unchanged.
Issues Found
None in the changed behavior. CI note: the PR description check was failing because the human-only PR description fields were not completed, which needs a human update rather than an agent edit.
This QA review was generated by an AI agent (OpenHands) on behalf of the user.
HUMAN:
AGENT:
Why
The agent-server file browser endpoints
GET /api/file/search_subdirsandGET /api/file/homeunconditionally skip dot-entries (entry.name.startswith(".")). GUIs built on these endpoints (e.g. agent-canvas' workspace picker) therefore cannot browse or select hidden directories such as~/.config,~/.ssh, or a hidden repo. There was no server-side way to opt in to seeing them.This is the backend half of "Option B" for OpenHands/agent-canvas — letting users add a hidden directory as a workspace via a "Show hidden folders" toggle.
Summary
include_hiddenquery param (defaultFalse) toGET /api/file/search_subdirs. Whentrue, dot-directories are listed; files and symlinks are still skipped.include_hiddenparam toGET /api/file/homeso hidden top-level directories can appear infavorites.include_hidden=False), so this is fully backward-compatible — no existing client is affected.Issue Number
Relates to OpenHands/agent-canvas hidden-workspace support (Option B).
How to Test
Manual:
Evidence (local run):
uv run pytest tests/agent_server/test_file_router.py -q→28 passed(incl. newtest_search_subdirs_include_hidden_lists_dot_directoriesandtest_get_home_include_hidden_lists_hidden_favorites).uv run ruff check/ruff format --check→ clean.Type
Notes
Follow-up wiring (separate PRs):
@openhands/typescript-client: forwardinclude_hiddenfromFileClient.searchSubdirectories(extendFileSearchSubdirsOptions) and accept it ongetHome; release a new version.includeHiddenthroughuseSearchSubdirs/useHomeDirectory.This PR was created by an AI agent (OpenHands) on behalf of the user.
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:0effd62-pythonRun
All tags pushed for this build
About Multi-Architecture Support
0effd62-python) is a multi-arch manifest supporting both amd64 and arm640effd62-python-amd64) are also available if needed