* fix: use 0o644 for inbound media files to allow sandbox read access
Inbound media files were saved with 0o600 permissions, making them
unreadable from Docker sandbox containers running as different users.
Change to 0o644 (world-readable) so sandboxed agents can access
downloaded attachments.
Fixes#17941
Co-Authored-By: Claude <noreply@anthropic.com>
* test(media): assert URL-sourced inbound files use 0o644
* test(media): make redirect file-mode assertion platform-aware
* docs(media): clarify 0o644 is for sandbox UID compatibility
---------
Co-authored-by: zerone0x <zerone0x@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Address Greptile review: add explicit "outside-workspace" case to
toSaveMediaSourceError so it returns "Media path is outside workspace
root" instead of the generic "Media path is not safe to read".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When editing a file outside the workspace root, SafeOpenError previously
used the "invalid-path" code with the message "path escapes root". This
was indistinguishable from other invalid-path errors (hardlinks, symlinks,
non-files) and consumers often fell back to a generic "not found" message,
which was misleading.
Add a new "outside-workspace" error code with the message "file is outside
workspace root" so consumers can surface a clear, accurate error message.
- fs-safe.ts: add "outside-workspace" to SafeOpenErrorCode, use it for
all path-escapes-root checks in openFileWithinRoot/writeFileWithinRoot
- pi-tools.read.ts: map "outside-workspace" to EACCES instead of rethrowing
- browser/paths.ts: return specific "File is outside {scopeLabel}" message
- media/server.ts: return 400 with descriptive message for outside-workspace
- fs-safe.test.ts: update traversal test expectations
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cleanOldMedia() only scanned the top-level media directory, but
saveMediaBuffer() writes to subdirs (inbound/, outbound/, browser/).
Files in those subdirs were never cleaned up.
Now recurses one level into subdirectories, deleting expired files
while preserving the subdirectory folders themselves.