The Docker refusal on POST /api/sessions/:id/folders tested only `~/`, but
listFolder also expands a bare `~` to this host's home, so a Docker case
could list and serve the host home through it (the preview's "Show folder"
for a `~/x.png` link reached it with no typing). A bare `~` now gets the same
400.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
sshd runs the command in the remote user's login shell. On Hufflepuff cd is
an alias that echoes the directory, so the listing script captured the folder
path twice and zsh aborted on the glob: every folder grid over ssh failed
with "remote host unreachable: no matches found". Wrap the script in sh -c,
as the transcript read already does.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A Docker case is a local session for the file routes, but `~` inside the
container is the container's own seeded home. Expanding `~/x.png` on the host
opened a different file than the one the agent printed. The attachment POST
and POST …/folders now answer 400 for `~/` when the session has `docker` set
(absolute paths are unaffected: the workspace is a bind mount).
Docs: the resolved-target gate, the route-parameter limit, the listing join
rule and the Docker `~/` refusal in architecture-invariants.md,
security-architecture.md and CLAUDE.md.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Folder routes only looked at the requested name, so `shot.png -> ../secret.bin`
and `notes.txt -> ../history` were served (registration answers 400 for both),
and a FIFO or device behind a supported name parked a worker on an open that
never returned. After resolving, the target's extension must be supported and
it must be a regular file (locally fs.stat, remotely the probe kind).
A 101..255-character file name answered 404 "Route not found": Fastify caps a
path parameter at 100 characters. The server now sets routerOptions.maxParamLength
(MAX_ROUTE_PARAM_LENGTH, shared with the route test harness) and a wiring guard
pins it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Multi-user: a non-admin's folder must resolve inside their own userSpacePath
or the session's workingDir (folderAllowedForUser, the path picker's rule),
checked on the resolved folder before it is read and again on each served
file after symlink resolution; 403 otherwise.
- listFolder probes the local folder with probePathKind({ pastCap: true })
first (unknown -> 503 via describeUnknownPath, never 404), then async
fs.realpath; resolveFolderFile uses probePathKind instead of fs.stat.
- The guard runs on the realpath before readdir.
- FOLDER_PATH_LINK_PATTERN ends a folder at ':' (quadratic `://` rescan on
colon runs; PATH=/a:/b linked as one folder).
- Load counters drop stale preview / folder-grid responses; closing the grid
invalidates a listing still in flight.
- validFolderFileName refuses a leading dot; the remote listing matches
extensions in any case.
- Docs: folder grid in the z-index lists; multi-user rule and bounded probe in
the invariants and security docs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Folder paths in the terminal and the response viewer are now links
(rooted or ~/ without an extension, relative with a trailing slash); a
click opens a grid of the folder's supported files, newest first, with
subfolder chips and a parent button. A tile opens the file preview,
which pages through the folder with ‹ › / ←/→; the preview of any file
gets a "Show folder" button.
Server: POST /api/sessions/:id/folders grants + lists a folder (local
fs or one ssh round trip for a remote case) under the attachment guard;
GET …/folders/:folderId/files/:name/(raw|preview|thumbnail) serves one
file by bare name through resolveServableAttachmentPath, so browser
requests still never carry a path. Grants live in their own small
registry so a big folder cannot evict the session's attachments. The
attachment raw route's body is now the shared serveServableRaw helper.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>