* 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>
50 lines
4.5 KiB
Markdown
50 lines
4.5 KiB
Markdown
# 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.
|