Files
openchamber/packages/web/server/lib/fs/DOCUMENTATION.md
T
Serhii Dziupin 73c2f7bf23 fix(server): allow reading files through workspace-internal symlinks
Read-family fs routes (stat/read/raw/serve) rejected files whose canonical
(realpath) target escaped the project root, so a symlinked folder inside the
workspace (e.g. ~/test_folder -> /shared/test_folder) listed fine but every
file open failed with "Failed to open file".

Resolve symlinks before the containment check: paths that are lexically
inside the active workspace stay readable even when their realpath target
lives outside it, while direct paths outside the workspace (including
traversal and canonical-path requests) remain rejected and write/exec keep
the strict canonical boundary. The directory listing now returns entry
paths under the requested (user-visible) directory so the file tree hands
back addressable paths instead of canonical ones.

Refs OPE-235
2026-08-21 01:40:11 +02:00

3.7 KiB

FS Module Documentation

Purpose

Own filesystem API behavior for the web server runtime, including workspace-bound file operations, directory listing, reveal, and background command execution jobs.

Entrypoints and structure

  • packages/web/server/lib/fs/routes.js: route registration and runtime-owned state for /api/fs/* endpoints.
  • packages/web/server/lib/fs/search.js: fuzzy filesystem search runtime used by non-FS routes (for example project icon discovery).

Public exports

  • registerFsRoutes(app, dependencies) from routes.js
    • Registers all filesystem routes:
      • GET /api/fs/home
      • POST /api/fs/mkdir
      • GET /api/fs/read
      • GET /api/fs/raw
      • GET /api/fs/serve/:path(*)
      • POST /api/fs/write
      • POST /api/fs/upload
      • POST /api/fs/delete
      • POST /api/fs/rename
      • POST /api/fs/reveal
      • POST /api/fs/exec
      • GET /api/fs/exec/:jobId
      • GET /api/fs/list
    • Owns exec job queue state (execJobs) and lifecycle/TTL pruning.
    • Enforces workspace boundary checks with active project + worktree fallback support.
    • Read-family endpoints (stat, read, raw, serve) resolve symlinks before serving and allow paths that are lexically inside the active workspace even when their canonical (realpath) target lives outside it through a workspace-internal symlink (for example ~/test_folder -> /shared/test_folder). Paths that are not inside the workspace lexically are still rejected before any symlink resolution, and write/exec operations keep the strict canonical containment check.
  • createFsSearchRuntime({ fsPromises, path, spawn, resolveGitBinaryForSpawn }) from search.js
    • Returns { searchFilesystemFiles(rootPath, options) }.
    • Supports fuzzy matching, hidden-file handling, and optional git check-ignore filtering.

Composition contract with index.js

  • index.js provides composition-time dependencies only (platform primitives + callbacks such as resolveProjectDirectory, normalizeDirectoryPath, and buildAugmentedPath).
  • index.js no longer owns FS route handlers or FS exec job state.

Notes for contributors

  • Keep filesystem policy (workspace root checks, error mapping, exec timeout behavior) inside this module, not in the composition root.
  • Filesystem EPERM/EACCES failures use the stable reason: "os-permission" response marker. Policy denials such as workspace-boundary or missing-grant failures must not use that marker because a native folder picker cannot remediate them.
  • Read-only routes authorize the requested path against the workspace before resolving symlinks. A symlink reached through the workspace may therefore target a file outside it, while a directly requested outside path still requires an exact-path grant. Write routes keep canonical-target boundary checks.
  • If adding new /api/fs/* endpoints, add them in routes.js and extend this document.
  • GET /api/fs/list may resolve symlinks with realpath to read directory contents, but the response path and each entry path must stay in the caller's requested path space (path.join(requestedPath, name)). Returning real paths breaks file-tree expansion for directories reached through workspace symlinks.
  • POST /api/fs/upload accepts one application/octet-stream body with path and optional overwrite=true query parameters. The body streams into a same-directory temp file with a 100 MiB default cap configurable through OPENCHAMBER_FS_UPLOAD_MAX_BYTES; failed and oversized uploads clean up that temp file. New files commit through an atomic no-replace link, existing files return 409 unless overwrite is explicit, directory targets are rejected, and the destination parent resolves before writing so uploads cannot escape through workspace symlinks.