Skip to content

feat(files): download and upload files from the path picker - #631

Draft
shenlvkang-collab wants to merge 2 commits into
Ark0N:masterfrom
shenlvkang-collab:feat/path-picker-file-transfer
Draft

shenlvkang-collab wants to merge 2 commits into
Ark0N:masterfrom
shenlvkang-collab:feat/path-picker-file-transfer

Conversation

@shenlvkang-collab

@shenlvkang-collab shenlvkang-collab commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What changed

The path picker (the mobile keyboard's 📁 Path key) is the fast way to browse files: one folder per request, any allowed root, previews, typed-path jump, sort. It could not move files, though, so the Files panel was still needed for that, and a desktop browser could not open the picker at all. This PR fills both gaps.

  • Download: every file row gets a ⬇ link (GET /api/filesystem/download). Any regular file the picker can list is sent as an attachment and never rendered.
  • Upload: an ⬆ Upload button sends one or more files from the device into the folder on screen (POST /api/filesystem/upload, multipart field file). XHR shows progress; afterwards the folder reloads and the last upload is selected, so Insert Path puts it on the prompt in one tap.
  • Desktop entry: a 📂 quick-browser button in the Files panel header opens the same picker.
  • Turned on by a new fileTransfer picker option: on for the Path key and the Files panel button. Link Existing stays browse-only, and so does a fetchListing (container) picker.

Safety

  • Both routes resolve through the existing resolveFilesystemPickerPath(): allowlisted roots, realpath containment, the hidden-entries opt-in, isBlockedPickerPath, the sessionId ownership check and multi-user root scoping. They reach exactly what browse can list.
  • Upload is the picker's only write:
    • same-origin multipart check: the paste-image check, moved into a shared isSameOriginMultipartRequest() helper with unchanged behaviour
    • per-IP bucket (60/min)
    • bounded probe of the target folder (fix(cases): bound path probes for linked workspaces and session creation, so an unreachable mount cannot freeze the server #516)
    • sanitized basename (no separators, control characters or leading dots)
    • blocked-name check on the final name, so credentials.json or a key file is refused
    • O_CREAT|O_EXCL|O_NOFOLLOW creation with (n) suffixes: it never overwrites and never follows a planted symlink
    • truncated or failed streams are unlinked
    • 1GB cap (CODEMAN_MAX_PICKER_UPLOAD_BYTES)

Validation

  • New test/routes/filesystem-transfer.test.ts (24 tests, real temp dirs). Download covers: allowed file, a path outside the roots, a symlink escape, a sensitive file, a folder. Upload covers: writes and returns the path, never overwrites, does not follow a planted symlink, strips path components, refuses an out-of-root folder, a sensitive name, a cross-site request, an unknown session and the wrong field. Plus the name sanitizer.
  • The file-routes, path-picker UI, session-routes (paste-image), unreachable-mount and mobile composer suites pass.
  • npm run typecheck, npm run lint, Prettier and check:frontend-syntax pass.
  • Full npm test locally: 11179 passed. The only failures were the 4 spreadsheet/public-asset files, which need the exceljs vendor assets that this worktree's node_modules lacked.
  • Playwright against a built server (isolated CODEMAN_INSTANCE), 19 checks, desktop 1400px and phone 412px:
    • the Files panel button opens the picker at the workspace
    • a download saves byte-identical content
    • a two-file upload lands, with no overwrite (readme (1).txt), and the listing refreshes and selects the upload
    • Insert closes the picker
    • the phone dialog has no horizontal overflow
    • the Link Existing picker shows neither Upload nor download links
    • no page errors
  • Multi-user check on a built server: a regular user limited to one linked workspace saw only that root, could upload and download inside it, and got 403 for another folder (even one listed in CODEMAN_FILE_PICKER_ROOTS) and for Home.

Docs: CLAUDE.md path-picker bullet, docs/architecture-invariants.md#filesystem-path-picker, docs/wiki/Working-With-Files.md. Changeset: minor.

🤖 Generated with Claude Code

The path picker (mobile Path key) is the fast way to browse: one folder per
request, any allowed root. It could not move files, so the Files panel was
still needed for that, and a desktop browser could not reach the picker at all.

- GET /api/filesystem/download: any regular file the picker can list, sent as
  an attachment.
- POST /api/filesystem/upload: one multipart file into the folder on screen.
  Same-origin check (shared with paste-image), per-IP rate bucket, bounded
  folder probe, sanitized basename, blocked-name check, O_EXCL|O_NOFOLLOW
  creation with " (n)" suffixes (never overwrites, never follows a planted
  symlink), partial files unlinked. 1GB cap (CODEMAN_MAX_PICKER_UPLOAD_BYTES).
- Both resolve through resolveFilesystemPickerPath(), so they reach exactly
  what browse can list, including the multi-user root scoping.
- Picker `fileTransfer` option: a download link per file row and an Upload
  button (multi-select, XHR progress) that reloads the folder and selects the
  last upload. On for the Path key and the new quick-browser button in the
  Files panel header; Link Existing stays browse-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shenlvkang-collab shenlvkang-collab changed the title test-permission-probe-ignore feat(files): download and upload files from the path picker Oct 14, 2026
The key already exists further down the table; an object literal keeps the
last one, so the first copy was dead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant