* feat(stacks): browse and edit mounted volume files in the explorer
Reposition the stack file explorer around runtime configuration access:
discover a stack's declared mounts and expose each as a safe, stack-scoped
file root. The explorer opens on a Volumes group (bind mounts and named
Docker volumes) by default, with the stack source directory as a secondary
group, on a "Files & Volumes" tab.
- Discover roots from the rendered effective compose model; resolve named
volumes to their Docker name and browse/edit them through the hardened
helper container, with bind mounts handled directly when reachable.
- Re-derive the allowed roots server-side on every file operation and match
the client root id against them, so a request can never address a path the
stack did not declare. Block dangerous host mounts and binds that overlap
Sencho's managed directories; reject writes to read-only mounts.
- Thread an optional root id through the existing file endpoints and an
opaque, parseable optimistic-concurrency token through read, conflict,
and write, for both filesystem and helper backends.
- Keep compose and env file protection on the stack source root only.
* fix(stacks): theme the Files & Volumes root switcher
Replace the raw native select in the file-root switcher with the design
system Select component. The native control did not honour the dark theme,
so the panel rendered white with unreadable text. The themed Select gives a
dark popover with grouped Volumes / Stack source labels and disabled items.
* fix(stacks): contain the bind-root probe and de-taint the file-op error log
Gate the volume-root bind probe's realpath/stat behind a compose-base
containment check (mirroring the storage host-path probe) so they never run
on an unvalidated host path; a source outside the compose dir is unreachable
in the containerized deployment anyway and is reported non-accessible without
touching the filesystem. Log the helper-backed file-op failure through a
constant format string with sanitized arguments instead of an interpolated
template literal.
* fix(stacks): inline the bind-probe containment guard at the fs sinks
The wrapped containment predicate was not recognized as a path barrier, so
the bind probe's realpath/stat still flagged as uncontrolled-data-in-path.
Inline the path.resolve + startsWith check directly at each filesystem sink
(and re-check the resolved canonical before stat, so a within-base symlink
that resolves outside the compose dir is also rejected).
* fix(stacks): harden file-root lifecycle, upload race, and helper errors
Address review findings on the Files & Volumes feature:
- Invalidate the file-root allowlist on stack create/delete/import/from-git
(wire StackFileRootsService.invalidateNode into invalidateNodeCaches), so a
stack deleted and recreated under the same name cannot serve the old stack's
roots from the 15s cache.
- Use the atomic exclusive write for a non-overwrite upload so a file created
by another writer after the existence check is not silently clobbered.
- Let the helper's real cd errno through and map permission failures to 403
consistently across list/stat/read/write/mkdir/delete/pathKind, instead of
reporting EACCES as 404/500; pathKind no longer reports a permission-denied
parent as absent.
- Document the realpath-then-open TOCTOU as a known, pre-existing limitation of
every file op (O_NOFOLLOW is not viable because config volumes legitimately
contain symlinks); the bind root is contained to the compose dir and the op
requires stack:edit.
- Docs: drop a missing screenshot reference and correct the protected-file
delete behavior (stack-root compose/.env cannot be deleted via the explorer).
* feat(files): move files and folders across directories in the stack explorer
Add a cross-directory move to the stack file explorer. Files and folders can
be relocated either through a "Move to..." context-menu item that opens a
folder-picker dialog, or by dragging an entry onto a folder node (or onto the
root area to move it to the stack root).
The backend reuses the existing rename endpoint: renameStackPath now resolves
both ends through the leaf helper, so a symlink moves as the link entry rather
than its target, and it guards against moving a directory into its own subtree.
A cross-filesystem rename surfaces as a clean 409 instead of a 500. Protected
root files (compose / docker-compose / .env) stay put. Moving the open file, or
a folder containing it, deselects the viewer; a move that would discard unsaved
edits is blocked with a clear message.
* fix(files): fold case in move guards and keep the move dialog open on failure
Harden the cross-directory move against case-insensitive filesystems and fix a
dialog dismissal edge:
- Protected root files (compose / docker-compose / .env) were gated by an exact,
lowercase name match. On a case-insensitive filesystem a request like
COMPOSE.YAML resolves to the real compose.yaml and slipped past the gate, so a
protected file could be moved out of the stack root via the API. The gate now
folds case on case-insensitive platforms; Linux stays case-sensitive, where a
differently-cased name is a distinct, unprotected file.
- The directory-into-descendant guard compared resolved paths case-sensitively,
so a source supplied with non-disk casing skipped the guard and fell through to
an opaque OS error (500) instead of a clean 400. The comparison now folds case
the same way.
- The move dialog closed after awaiting the move regardless of outcome, so a
blocked move (unsaved edits) or a failed move dismissed the picker as if it had
succeeded. The shared handler now reports success and the dialog only closes on
an actual move.