Files
openchamber/packages/web/server/lib/fs/DOCUMENTATION.md
T
Steffen MächtelandBohdan Triapitsyn 3df97908fe feat(chats): relocate managed chat worktrees via OPENCHAMBER_CHATS_DIR (#3135)
* feat(chats): relocate managed chat worktrees via OPENCHAMBER_CHATS_DIR

Projectless-chat worktrees were hard-pinned to
<home>/.config/openchamber/chats: the UI joined the path client-side,
workspace checks allowed only the config root, and identification matched
the literal path segment. When the OpenCode server runs as a separate
user (UID-separated setups), that root is unreachable — every chat
session answered HTTP 500 (EACCES on the session directory).

The server now owns the chats root. OPENCHAMBER_CHATS_DIR relocates it
(default unchanged: <config root>/chats); /api/fs/home answers
{ home, chatsRoot }; fs workspace checks accept the managed chats root
next to the config root; the client resolves the root from the server
(per-runtime cached, warmed at bootstrap so sync classification sees it)
and falls back to the home join for older servers.

Refs #3130

* chore: trim added comments to local precedent

* fix: forward managedChatsRoot through feature-routes-runtime to registerFsRoutes

* fix(chats): await the root warm-up and keep the legacy chats root owned

Review feedback on #3135:

- bootstrapGlobal now awaits warmChatsRootDirectory, so synchronous
  session classification never sees an empty root cache (relocated
  sessions were grouped as project sessions when the session list
  outran /api/fs/home).
- managedProjectRoots keeps the legacy <config root>/chats entry next to
  OPENCHAMBER_CHATS_DIR, so memory ownership of existing chats survives
  relocation.

* fix(chats): distinguish chats-root fetch failure from older servers

* fix(sync): rehydrate managed chat sessions after the chats root warms

* fix(fs): pass managed roots through the symlink and git-dirs path checks after the main merge

* docs: drop changelog edits; changelog is the maintainer's release-time work

* fix(chats): keep legacy chat directories deletable while the root is relocated

* fix(chats): resolve roots before cleanup and initial session loads

* test(chats): type runtime spies against actual SDK contracts

---------

Signed-off-by: Steffen Mächtel <info@steffen-maechtel.de>
Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
2026-09-05 19:24:52 +03:00

4.5 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
      • GET /api/fs/git-dirs — shallow nested git repository discovery for the Git tab (depth- and visit-capped readdir walk; .git directory, file, or symlink marks a repository boundary; junk directories and symlinks are never descended into)
    • Owns exec job queue state (execJobs) and lifecycle/TTL pruning.
    • Enforces workspace boundary checks with active project + worktree fallback support.
    • The active project directory is validated with fs.realpath, so when the project root is itself a symlink the workspace base no longer matches the paths the client sends. Workspace resolution therefore retries against the raw directory the client requested (requestedDirectory from resolveProjectDirectory) before falling back to worktree roots. Symlinks are still resolved afterwards, and write/exec routes keep their canonical containment check against the resolved base.
  • 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.
  • Workspace checks accept, besides the active workspace and its worktrees, the managed roots: the OpenChamber config root and the managed chats root (managedChatsRoot dependency; OPENCHAMBER_CHATS_DIR upstream, default <config root>/chats). Chat worktrees may legitimately live outside every project workspace.
  • GET /api/fs/home answers { home, chatsRoot }. chatsRoot is the server-resolved managed chats root; clients must use it instead of joining home + the well-known segment (a relocated root does not contain that segment).
  • 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.