Repository navigation
fix(editor): restored tabs for files outside your projects no longer fail with Access denied - #24489
Conversation
A file opened from outside every project (e.g. ~/notes.txt from the floating workspace) read through an in-memory grant. After a restart the restored tab only renewed that grant when it stored a full path, so a tab saved relative to the floating workspace folder failed with "Access denied" and Retry repeated it. Single-file reads (read, stat, exists) and open-editor-tab saves now resolve a path outside every project in place. Paths inside a project keep the full containment check, so a project's symlinks still cannot escape it, and every other write stays inside projects.
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Path resolver for user-named files — adds
resolveUserNamedPathinsrc/main/ipc/filesystem-auth.ts:149. It tries the existing containment check and, only when the candidate is outside every root, returns the resolved path with no realpath and no in-memory grant; non-absolute paths are refused. - Read handlers switch resolvers —
fs:readFile,fs:stat,fs:pathExists, andfs:pathsExistnow useresolveUserNamedPathinstead ofresolveAuthorizedPath(filesystem-read-handlers.ts). - Editor-save write flag —
fs:writeFileacceptssavesOpenEditorFileand routes only editor saves throughresolveUserNamedPath; every other write keepsresolveAuthorizedPath(filesystem-write-handlers.ts:45). - Renderer plumbing —
writeRuntimeFilegains an options bag and strips the flag whencontext.connectionIdis set, so SSH writes never carry it (runtime-file-mutation-client.ts);editor-save-queue.ts:113sets it on local saves. - Tests — new
src/main/ipc/filesystem-user-named-path.test.ts(6 tests, verified green locally) plus renderer call-shape updates.
ℹ️ Outside-project access now depends on fs:authorizeExternalPath staying ungated
The change is not a widening today because any renderer can already grant itself any absolute path: fs:authorizeExternalPath is an unauthenticated IPC that records the path straight into authorizedExternalPaths. The new fallthrough returns exactly resolve(targetPath), which is the same value that handler stores, so every target it permits was already reachable. This is load-bearing — if fs:authorizeExternalPath is ever given a sender check or a user-gesture requirement, resolveUserNamedPath becomes a genuine bypass for symlink escapes and for a renderer-set savesOpenEditorFile.
Technical details
# The invariant that makes this change safe
## Affected sites
- `src/main/ipc/filesystem/filesystem-write-handlers.ts:106` — `fs:authorizeExternalPath` handler, no sender/trust/gesture check.
- `src/main/ipc/filesystem-auth.ts:38` — `authorizeExternalPath` records `resolve(targetPath)` (and best-effort `realpathSync`) with no validation.
- `src/main/ipc/filesystem-auth.ts:149` — `resolveUserNamedPath` fallthrough returns `resolve(targetPath)` for outside-root paths.
- `src/main/ipc/filesystem/filesystem-write-handlers.ts:45` — `savesOpenEditorFile` is taken directly from IPC args.
## Required outcome
- Either keep `fs:authorizeExternalPath` ungated (current state, so no action needed), or, if it is ever gated, move the outside-root allowance behind an equivalent check.
## Suggested approach (optional)
- No change requested for this PR. Carrying this note forward so a future gate on `authorizeExternalPath` is recognized as security-relevant to `resolveUserNamedPath`.
## Open questions for the human (optional)
- The PR discloses that a remote path misrouted to the local read path would now read a same-named local file instead of being denied. The loader's owner-not-ready guard covers the restored-tab path; is any other entry point able to send a remote path without a `connectionId`?deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (127)
💤 Files with no reviewable changes (19)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe pull request replaces external-path authorization grants with typed local access shapes. Main-process handlers now resolve desktop, user-named, and document-resource paths and validate regular files. Runtime clients and renderer features pass access metadata through reads, writes, stats, previews, image caches, editor tabs, attachments, drops, and link handling. Tests cover path boundaries, symlinks, bounded reads, restored tabs, and access-aware image caching. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The change lets restored editor tabs for files outside a project load and save without "Access denied", and it keeps containment checks for project files. No actionable merge-blocking issue remains; the author's reported gaps (a real app restart, Windows, Linux) are normal verification items. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to The permission model changes across file opening, saving, and previews. A compromised application interface can still request writes outside projects, but that capability predates this change. Several controls improve; the wider migration remains only partially verified. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❓ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation Docstring coverage is 39.39% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 66 functions across 59 files. (58 skipped: 6 unsupported, 52 over the file limit.) ✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: f47e8b1a-9a0b-4163-82d9-a8c3cda7f8b8
📒 Files selected for processing (13)
src/main/ipc/filesystem-auth.tssrc/main/ipc/filesystem-user-named-path.test.tssrc/main/ipc/filesystem/filesystem-read-handlers.tssrc/main/ipc/filesystem/filesystem-write-handlers.tssrc/preload/api/fs-bridge.tssrc/renderer/src/components/editor/editor-autosave-conflict-flow.test.tssrc/renderer/src/components/editor/editor-autosave-controller.test.tssrc/renderer/src/components/editor/editor-save-queue.tssrc/renderer/src/components/editor/restored-editor-owner-save-lifecycle.test.tssrc/renderer/src/components/editor/useEditorPanelFileContentLoader.tssrc/renderer/src/runtime/mobile-markdown-bridge.test.tssrc/renderer/src/runtime/runtime-file-client-mutation-ownership.test.tssrc/renderer/src/runtime/runtime-file-mutation-client.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.
…eads-files-outside-projects
…path shape Problem: a file opened from the floating workspace (e.g. ~/notes.txt via Cmd-click in the floating terminal, the floating markdown picker, or a .md opened from the OS) loads until restart, then shows "Access denied: path resolves outside allowed directories". Main's external-path grants live only in memory. On restore the editor re-granted only tabs that stored an absolute path, but floating tabs store a path relative to the floating root (~ by default), which is deliberately not an authorized root, so they were never re-granted. Restored floating notebooks also failed to start a kernel. The previous commit on this branch let main read and save any path outside a project without a grant. That widened fs:readFile/stat/pathExists for every caller, including automatic reads of untrusted content (markdown preview images), which opened a Windows UNC credential leak and a /dev/zero main-process memory blowup. This reverts that model entirely. Fix: one helper decides which client-local path a tab needs re-granted by ownership: a floating-workspace tab, or a tab stored outside its own project. It never grants paths a local project root covers (a grant would also authorize a project symlink's outside target), and skips SSH-owned, runtime-owned and not-yet-hydrated owners. Every reader that can touch a restored tab before or without the editor loader uses it: the loader, the restored dirty-tab conflict scan, and the paired-mobile markdown bridge.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes (since the prior pullfrog review at 19fdcf6)
- Reverted the main-process approach entirely —
resolveUserNamedPath, the read-handler swaps (fs:readFile/stat/pathExists/pathsExist), thefs:writeFilesavesOpenEditorFileflag, and the preload/mutation-client plumbing are all gone; reads and writes are back to grant-required. This closes the outside-root widening the earlier commit introduced (automatic reads of untrusted content, e.g. Windows UNC credential leak and/dev/zeroblowup). - Added a renderer-side grant helper —
getEditorTabExternalPathGrantTarget/refreshEditorTabExternalPathGrantinsrc/renderer/src/lib/editor-tab-external-path-grant.tsre-derive the client-local path a tab needs re-granted: always for floating-workspace tabs, only for absolute-stored tabs whose owner is a resolved local worktree, and never when a local project/folder root, SSH target, or runtime environment already owns the path. - Wired it into the readers that can touch a restored tab before or without the editor loader —
useEditorPanelFileContentLoader.ts(new root-relative branch),editor-restored-tab-conflict-scan.ts(before the dirty-tab verify read), andruntime/mobile-markdown-bridge.ts. - Tests — helper unit tests for floating/local/SSH/runtime/unhydrated owners, grant-before-read ordering in the conflict scan and mobile bridge, and the floating loader path; the helper and conflict-scan suites pass locally.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…eads-files-outside-projects
…dy covers Problem: the restore re-grant helper decided "already inside a project" in the renderer from its worktree list. At startup that list only holds repos the session references, so a floating tab inside an unlisted repo was granted (including a symlink's outside target), and the renderer's path matcher disagrees with main's on WSL \\wsl$ vs \\wsl.localhost, which stranded a folder-workspace tab with "Access denied" after restart. The helper also treated a folder workspace with a missing or ambiguous host as local. Fix: the renderer now decides only by owner (a floating-workspace tab, or a tab stored outside a project whose owner is explicitly local) and asks main with `skipIfInsideAllowedRoots`. Main checks the path against its own allowed and registered roots, in both the named and canonical-parent spelling against both root spellings: a path a project covers gets no grant, an alias spelling of a project path gets only that spelling, and a project symlink's outside target is never granted. Explicit-open grants (Cmd-click, drag, explorer) are unchanged. Tests now prove each reader waits for the grant before reading.
There was a problem hiding this comment.
Important
Re-review of 303b7ab found one security-guard regression in the new main-process grant helper: an alias-named path whose parent traverses an inner directory symlink out of the project falls through to a plain grant, recording the outside target in authorizedExternalPaths. I reproduced it locally (temporary vitest case, since removed). One inline comment with the trace, required outcome, and a suggested fix.
Reviewed changes (since the prior pullfrog review at 9e47d99)
- Moved the "does a project root already cover this path?" decision from the renderer into main — the renderer helper now only identifies the owner, and
fs:authorizeExternalPathacceptsskipIfInsideAllowedRootsto runauthorizeExternalPathOutsideAllowedRoots. - Added
authorizeExternalPathOutsideAllowedRoots— a textually-in-root target gets no grant; an alias-spelled project path gets only the alias spelling (never its realpath); everything else falls through to the existing grant. - Simplified the renderer owner predicate — replaced the
isWorktreeConnectionResolved/findWorkspaceFileRoutechecks withgetConnectionIdForFile(...) === null, so a folder workspace with an unknown or ambiguous owner now fails closed instead of guessing local. - Tests — new main grant suite (outside file, leaf/directory symlink denial, two-spelling alias matrix) plus renderer helper, floating-tab loader, conflict-scan, and mobile-bridge ordering tests.
I independently checked the renderer owner predicate change for lost grants: the only behavior removed is the folder-workspace unknown/ambiguous-owner local guess, which the loader's WORKTREE_OWNER_NOT_READY guard and main's folder allowed-roots both make harmless. No regression for a provably-local restored tab.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…or's real path Problem: the restore re-grant decided "inside a project" from the named path and the real path of its parent only. When a tab path crossed a project directory symlink and named the project through a spelling that was neither the registered root nor its realpath (a second alias, a `..` segment, a case variant on a case-insensitive disk, a /var-style alias of an ancestor), no check matched, the path took the full grant, and the symlink's outside target became readable and writable. Fix: a path is inside a project when the named path is inside a root, or the real path of any ancestor folder is inside a root in its registered or real spelling. Such a path gets at most its named spelling, never its realpath. An ancestor that fails to resolve for any reason other than "missing" now fails closed to the named-spelling grant instead of falling through to the full one. Tests cover each spelling; the non-symlink case also runs on Windows.
…eads-files-outside-projects
Problem: fs:readFile stat'ed a path and then read it to EOF. A character device such as /dev/zero reports size 0, passes the size limit and never ends, so the main process buffers until memory runs out; a FIFO hangs the open. Writes could also target an existing device or FIFO. Fix: every local fs:readFile (editor and log snapshot) opens the path once, non-blocking, refuses anything but a regular file, and reads the size check, binary probe and content from that same handle, capped at the limit even if the file lies about its size. The AI Vault log tail opens non-blocking too, and fs:writeFile refuses an existing non-regular target.
Problem: main decided every local file request against one allow-list plus a set of in-memory grants the renderer had to recreate after every restart, so a file the user opened outside a project (for example from the floating workspace) was denied once Orca restarted. This adds the request shape the common pattern uses, alongside the grants for now: - no shape (the default): the path must be inside a project root main recognises, symlinks included. Desktop requests also accept the app-owned floating-workspace folder; paired-client RPC never does. - user-file: a single file the user named by absolute path, used in place. Only fs:readFile/stat/pathExists and saving (fs:writeFile) accept it. - document-resource: an image or PDF a document references, limited to every project root when the document is in one, else to the document's folder, and refused by path text before any disk or network access. Notebook kernels and AI Vault log tails check their open file as user-named.
Problem: after a restart, a tab opened outside every project (a floating workspace file, a file opened by absolute path, an OS-opened markdown) could only be read if the renderer first re-granted its path, and readers that ran before the editor loaded the tab had no grant at all. The renderer now says what kind of request it is making, and main checks that: - A persisted tab opened outside its owner's root (floating workspace, or an absolute stored path) whose owner is explicitly local reads and saves as a user-named file, from every reader: the editor loader, the restored-tab conflict scan, the change banner and compare dialog, the paired-phone markdown bridge and the save queue. Project tabs stay inside their root. - Clicks, drops, typed paths and browser-opened notebooks stat as user-named. - Markdown preview and rich-editor images are document resources, limited to the document's roots or folder. Images the user pasted or attached into a chat show as user-named; agent images stay inside the project. - The image cache keys on the shape, so one shape's image never answers another's request. A ratchet test lists every renderer file allowed to create a user-named request.
Problem: main kept a set of paths the renderer had asked it to allow (fs:authorizeExternalPath). The set lived only in memory, so a file the user opened outside every project could be read until Orca restarted and was then denied, and every new reader of a restored tab had to remember to recreate the grant first. Three rounds of re-deriving grants at restore each found another reader or path spelling it missed. Now that every desktop request declares its shape, nothing needs a grant: - delete the grant set, authorizeExternalPath, the restore re-grant from the earlier commits on this branch, the fs:authorizeExternalPath channel and its preload and web-client entries; - delete every renderer grant call (terminal and markdown link clicks, drops, typed paths, the file explorer, AI Vault logs, chat attachments, browser notebooks) and every main one (floating markdown picker and folder, OS-opened markdown, keybindings.json, pasted images, import and upload sources); - the floating workspace's picker-approved folders stay a terminal-cwd allowlist only. Main now holds no per-path permission, so a restart can't change any answer.
…ed files
Problem: a file inside a project that is a symlink to something outside it
opened fine from the file explorer or a terminal Cmd-click, then showed
"Access denied" after a restart: its tab was stored as a project file, and a
project request is refused when it resolves out of the project. A folder link
out of the project expanded in the explorer until restart and then failed with
a raw access error.
Now the click decides and the tab keeps that decision. Both gestures stat the
path inside the project first; if only the user-named check passes, the path
leads out of the project:
- a file opens by its absolute path, so it reads and saves as a file the user
named, the same before and after a restart;
- the explorer does not follow a folder link out of the project and says so
("This folder links outside the project, so it can't be opened here.").
Paths that stay inside the project still open as contained project tabs. Also
drops the AI Vault "path not authorized" message, which nothing shows now.
The FileContent casts in the editor loader and the paired-phone markdown bridge were never needed (the read result is already assignable). Tests stub window.api through vi.stubGlobal and pass narrow stores without casting; the one test store that still needs a cast states why.
…eads-files-outside-projects
This comment has been minimized.
This comment has been minimized.
…adable Problems found in review: - Chat transcript images were trusted by message role: any user-role "[Image: source: <path>]" (an injected Claude record, `orca terminal send`, a paired client's image-ref) became an automatic user-named read as the row scrolled into view, of any file type, and on Windows a network-share path would have opened an SMB connection to that host. - A document image named like an image but linking to a text file (logo.png -> .env) was read as text. - Windows device names (NUL.png, COM1.jpg) passed the path-text check of the automatic image loads. - A project symlink leading out of the project, opened by a typed path, a tab-strip drop or a browser file:// notebook, was stored as a project tab and immediately refused. Fix: - New chat-image request shape for every transcript image and the composer preview, whoever's turn named it: an absolute local path whose requested and real targets are image files, a regular file, size-capped; network-share and device-namespace paths and Windows device names are refused by path text before any filesystem call. Pasted screenshots still show after a restart, and agent images outside the project now render. - Document resources check the real target's type too, and refuse Windows device names by path text. - Typed paths, tab-strip drops and browser notebooks stat through the same check as the explorer and terminal, and open an escaping link by its absolute path. - Tests pin the shape at the change banner, compare dialog, markdown preview and image prewarm; a second ratchet lists every file that can open a tab the tab rule reads as user-named, and its comment says what it can't see. - Stale grant wording removed.
…eads-files-outside-projects
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes (since the prior pullfrog review at 303b7ab)
- Deleted the in-memory external-path grant system —
authorizeExternalPath,authorizedExternalPaths, thefs:authorizeExternalPathIPC channel and every call site are gone;resolveAuthorizedPathno longer consults remembered grants. - Added renderer-declared request shapes —
src/shared/local-file-access.tsdefinesuser-file/document-resource; main dispatches them in the newfilesystem-request-shape.ts(resolveLocalFileRequestPath,resolveLocalWriteRequestPath,resolveDesktopAuthorizedPath, which adds the app-owned floating-workspace folder as a desktop-only root). - Hardened local reads — the new
local-regular-file-read.tsopens one bounded handle and refuses non-regular files (devices/FIFOs/dirs), closing the device-file blowup;document-resourcerefuses network paths by path text before any disk access. - Migrated every former grant site — markdown/rich-markdown images use
document-resource; drops, pastes, link clicks, clipboard temp images and the floating picker useuser-file; editor tabs (including the restored floating tab this PR targets) resolve througheditorTabFileAccess; notebook and log-tail reads use the user-named regular-file resolver. - Threaded the shape through the runtime clients — read/stat/write carry
access, andlocalAccess()strips it whenever aconnectionIdis set so SSH stays the remote host's call; the local image cache key now includes the shape. - Added regression coverage — new request-shape, network-path, regular-file-read, floating-tab loader, user-named save and a source-ratchet suite pinning the
user-fileimporters.
I re-derived the two concerns a prior round raised and both stay closed: content-referenced images cannot reach user-file (they use document-resource, which refuses network targets before touching disk), and the deleted authorizeExternalPathOutsideAllowedRoots fallthrough no longer exists. Two focused sub-reviews (content→user-file bypass; lost-grant regressions) found no new exploitable gap — link-target clicks are gestures equivalent to the old grant, and the newly-refused case (expanding a file-explorer directory symlink that resolves outside the project) is deliberate and test-covered.
ℹ️ PR description drifted from the final code
The "Why / Temporary" section still says directory listing, search, notebooks and log tails "still use the in-memory permission", but that permission and the fs:authorizeExternalPath channel no longer exist — those surfaces now resolve roots-only or via resolveUserNamedRegularFile. Worth a quick edit so reviewers are not looking for a mechanism that is gone.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
| throw new Error(CHAT_IMAGE_TYPE_MESSAGE) | ||
| } | ||
| // Why the real target too: `shot.png -> ~/.ssh/id_rsa` must not be read as an image. | ||
| const realTarget = resolve(await realpath(resolvedTarget)) |
There was a problem hiding this comment.
Network share contacted before rejection If a chat transcript references a local
shot.png symlink to a Windows network share, scrolling the image into view triggers a read without a click. The local path passes the first check, but realpath dereferences it before the resolved network path is rejected. This can initiate an unintended network connection.
How this was verified: Transcript images load automatically, and the resolver dereferences the local path before checking whether its target is a network path.
…eads-files-outside-projects
Problems found in review: - Two unit tests went red on this branch: the browser-share test still expected reads without a shape, and the rename test's electron mock had no app, which the desktop root check now needs. - The device-name check ran on the raw path, so `NUL.png\.` or `COM1.png\x\..` (reachable from markdown ``) reached the filesystem; a document image whose real target was a device name passed. - Chat images in a project that lives on a Windows network share no longer rendered, though the markdown preview showed them. - Any failed project check (a missing file, a dropped connection) was taken as "this link leads out of the project" and opened as an absolute tab. - Every local read allocated about 2 MiB, even for a tiny image. Fix: - Device names and device-namespace paths are checked on the resolved path and on the real target, for chat images and document resources alike. - A network-share path in an automatic load is read only inside a project root (the user chose that share when adding the project); anywhere else it is still refused by path text before any filesystem call. - Only main's "outside allowed directories" refusal marks a project path as leading out of the project; other errors surface as before. The message now lives in shared code so both sides agree on it. - Reads size their first buffer from fstat and confirm EOF with a 1-byte probe; a file that grows past its reported size is still read in bounded chunks up to the cap. - Fixed the two red tests.
…eads-files-outside-projects
…heck allows A full-path tab for a file inside a project (for example a link that leads out, opened by its absolute path) was renamed under the document-folder rule, which limited the new name to the file's own folder, although the same rename with no declared access could move it anywhere in the project. Any declared kind could be stricter than the default in the same way. Every desktop local file request now goes through one resolver, resolveLocalRequestPath: it runs the default project check first (roots, Orca's floating folder, symlink containment, outside-root path text refused before any filesystem call) and only on a refusal applies the declared kind's rule, which adds paths outside projects. Reads, saves, rename source and target, and import destinations all use it, so a new kind gets the rule for free. Automatic loads (document and chat) still refuse Windows device paths and names by text first, even inside a project; a device is never a file to show. The document-resource rule no longer needs its own project branch, and chat images no longer re-run the roots check for shares.
…eads-files-outside-projects
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes (since the prior pullfrog review at f0d898cc)
- Unified desktop file resolution behind
resolveLocalRequestPath— every desktop local read, save, rename and import now runs the default project/root check first and only applies the declared kind's rule when that check refuses, so a declared kind can no longer be stricter than the default project check.LocalRequestOperationnames the five path roles. - Simplified the document-resource and chat-image rules —
resolveDocumentResourcePathdropped its own project-roots branch (the default check now covers in-project targets), andresolveChatImagePathno longer re-runs the roots check for a network-share target. Both still refuse Windows device paths/names by path text before any filesystem call. - Moved rename/import onto the shared resolver —
fs:renameandfs:importExternalPathsreplace thedocumentFolderAccessPathguard withresolveLocalRequestPathcalls;resolveDocumentRenameSourcekeeps the exact-document constraint for the outside-project rename source. - Regression coverage — an in-project file renamed with
document-folderaccess is now allowed to move anywhere in its project, matching a request with no declared access; a matrix pins "a declared kind never refuses what the default project check allows" across every kind × operation; and a "default check runs first" test confirms no network share outside every project is contacted while resolving any declared kind. I ran the three touched main-process suites here (117 tests, all pass).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…e-share images - Renaming a file opened outside every project accepted a name in a subfolder (`archive/todo.md`), but the Undo and the rollback rename, declared from the moved file, were then refused and the file stayed moved. A rename under document-folder access must now land directly in the document's own folder (checked by path text before any filesystem call), so rename, Undo and rollback are symmetric. Image import still accepts the folder or a folder under it. Inside projects the default check still allows any in-project target. - A notebook opened through a link inside a project started its kernel in the link's folder instead of the real file's folder (main's behaviour), because notebook and AI Vault log-tail paths skipped the project check. Both now resolve through resolveLocalRequestPath (project check first, then the user-file rule). - A markdown file opened from a Windows share outside every project could not show the images beside it. Document images on a share are now allowed inside the document's own folder; the folder text check refuses every other host and share before any filesystem call. - resolveDesktopAuthorizedPath is async, so a synchronous failure in the default check rejects like any other refusal.
…eads-files-outside-projects
There was a problem hiding this comment.
ℹ️ The new commits are a clean fix for the outside-project rename/Undo asymmetry and the notebook real-folder case. No new critical or must-address issues — one security-residual observation and two wording nitpicks below.
Reviewed changes
Since the prior pullfrog review at 02f0290 (this delta is commit be3f9bb5e0 plus the origin/main merge 41450dac):
- Made outside-project renames symmetric — a
document-folderrename now has to land directly in the document's own folder (isSameFolder(dirname(target), documentFolder)), so a forward rename, its Undo declared from the new name, and a rollback all pass; project files, whose default check runs first, still move anywhere inside the project. - Resolved notebook and log-tail paths through the project check first —
resolveUserNamedRegularFilenow takes the store and delegates toresolveLocalRequestPath(..., {kind:'user-file'}, 'read'), so a notebook reached through an in-project symlink starts its kernel in the real file's folder;local-log-tailthreads the store for the same reason. - Allowed document images on the document's own network share —
resolveDocumentResourcePathdropped its unconditional network-share refusal; a target must still be inside the document's own folder by path text and by realpath, and every other host and share is refused by path text before any filesystem call. - Made
resolveDesktopAuthorizedPathasync — a synchronous throw while building the floating-workspace root now rejects and is swallowed like any other refusal instead of escapingresolveLocalRequestPath. - Tests — undo/rollback rename coverage, subfolder-rename and linked-subfolder-import refusals, a same-share read/refusal matrix, and a notebook project-link test asserting the real folder is the kernel cwd.
ℹ️ Same-share document images now realpath a share before the containment re-check can refuse a reparse point
Allowing a document image only inside the document's own folder is the right shape, and every spelling of a foreign host or share is still refused by path text. The one case the removed isNetworkSharePath(resolvedTarget) guard used to reject outright is a reparse point that lives inside the document's own folder but resolves to a different host: the folder gate passes, realpath follows the link (Windows may authenticate to the second host here), and only the realpath containment check refuses afterwards. This mirrors the already-disclosed local-symlink-to-share residual, now on the share side, and its exploitability could not be confirmed here without a Windows capture.
Technical details
# A reparse point inside the document's own share is realpath'd before the containment re-check
## Affected sites
- `src/main/ipc/local-file-access-resolution.ts:98` — the folder path-text gate passes for any target under the document's own folder, including a share path (the guard removed in `be3f9bb`).
- `src/main/ipc/local-file-access-resolution.ts:101` — `realpath(resolvedTarget)` now runs on a share target the previous unconditional `isNetworkSharePath(resolvedTarget)` refusal would have rejected outright.
- `src/main/ipc/local-file-access-resolution.ts:102` — the realpath containment re-check refuses a target that resolves to another host, but only after the call returned.
## Scenario
`documentPath = \\nas\notes\doc.md`, `targetPath = \\nas\notes\link.png`, where `link.png` is a
reparse point to `\\attacker.example\share\secret.png`. Gate passes at `:98`; `realpath` at `:101`
resolves off-share; the check at `:102` refuses — after the second host may have been contacted.
## Required outcome
- Decide whether a share reparse point whose real target leaves the document's share is acceptable
(as the disclosed local-symlink residual is), or should be refused before `realpath`.
- If accepted as-is, extend the PR body's residual note to cover it and add a test that a same-share
reparse point resolving off-share is refused.
## Open questions for the human
- Is the attached-document residual acceptable for a share the user already opened, given opening the
document already contacted that host?
- Does `fs.realpath` follow cross-server SMB reparse points on Windows? This could not be verified
here without a Windows capture.ℹ️ Nitpicks
src/main/ipc/local-file-access-resolution.ts:96— the comment says the folder text check refuses every other host and share "before a filesystem call could reach one", butisDescendantOrEqualcan itselfstatSyncon non-ASCII paths (filesystem-path-containment.ts:88) and the followingrealpathis a filesystem call by design. Reword so the guarantee matches what the code does.- The PR body's "Images on a Windows network share" bullet still says they load only inside one of your projects; same-share document images outside every project now load as well. A short update keeps the body from contradicting the code.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
… kernels as on main - Reverts the same-share document image rule from the previous commit. Its folder check compared hosts case-insensitively, so a host spelled with U+212A KELVIN SIGN (or a decomposed accent) passed as the document's own share and was contacted, reopening the network credential leak. Document and chat images on a share outside every project are again refused by path text before any filesystem call; tests now cover the look-alike hosts with zero filesystem calls. - Renaming a file opened outside every project into a project folder passed the project check, but its Undo (declared from the new path) was refused and the file stayed moved. When the rename source is allowed only as the opened document, the new name must now land directly in the document's folder even if a project would accept it (resolveLocalRenamePaths). - A notebook opened through a link outside every project started its kernel in the link's folder; main used the real file's folder. The kernel cwd is now the real file's folder in every case.
…eads-files-outside-projects
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review at 41450dac, this delta is commit c2e253d7 plus the origin/main merge 99b1aa4:
- Refused share images outside every project again —
resolveDocumentResourcePathrestored its unconditionalisNetworkSharePathrefusal, closing the same-share reparse-point residual from the prior round: a document's own network share no longer reaches automatic image loads, and look-alike hosts (U+212A KELVIN SIGN, decomposed accents) are refused by path text with zero filesystem calls. - Made an outside-project rename into a project undo-consistent — the new
resolveLocalRenamePathsresolves the target through the document-folder rule directly when the source was allowed only as the opened document, so a rename can no longer move a file into a project whose Undo (declared from the new name) would be refused. - Started a linked notebook's kernel in the real file's folder —
notebook:startKernelnowrealpaths the resolved user-named file, matching main's real-folder cwd for a notebook opened through a link outside every project. - Tests — added the look-alike-host share refusals, the rename-into-a-project undo case, and both linked-notebook cwds.
I ran the five touched suites (142 tests) and pnpm tc:node; all pass.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…eads-files-outside-projects # Conflicts: # src/renderer/src/components/editor/MarkdownPreviewBody.tsx
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review at 99b1aa44, the only commit is the origin/main merge 0d3db6dc (second parent 58cf72d48e); it had one conflict, in MarkdownPreviewBody.tsx.
- Merged
origin/main— brings the large-Markdown-preview rewrite (#24880) and other upstream work into the branch. - Resolved the
MarkdownPreviewBody.tsxconflict by keeping main's extraction — the sanitize schema moved to the newmarkdown-preview-plugins.ts, where the branch'sfile://protocol allowance and its "so the click handler can open the target" wording are preserved; both the static and virtualized preview bodies consumeMARKDOWN_REHYPE_PLUGINS, so previewfile://handling is unchanged. - Confirmed the PR's access threading survives the renderer rewrite —
useMarkdownPreviewComponentsstill passes the document-aware image component toVirtualMarkdownPreviewBody, andrich-markdown-extensions.tsstill threadsdocumentResourceAccess; the four touched Markdown suites (31 tests) pass.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
… keeps its access Renaming a document the user opened (floating workspace or full-path tab) now follows the user-file rule: the source must be the opened document, and the new path can be any absolute path, so a rename into another folder, a subfolder or a project works, and its Undo (declared from the moved file) comes back from there. Any other rename keeps the project check only. Remove the same-folder rename rule and its tests; image import stays in the document's own folder. After a move, a tab stored by its full path keeps its full path instead of being recomputed project-relative, so it keeps user-file access for save, the next rename, image insert and restore after restart. Folder moves go through the same remap. Also un-export unused resolver exports and avoid a copy for single-chunk reads.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review at 0d3db6dc, the delta is the single commit bbe1afe:
- Allowed a user-opened document to rename to any path —
resolveLocalRenamePathsnow resolves the opened document's rename asuser-file(both source and target), so the target is no longer confined to the document's own folder; this deliberately reverses the r11 folder-confinement rule. - Moved Undo symmetry to the renderer — the moved file is declared as the document on Undo and rollback (
documentFolderAccess(fromPath)/documentFolderAccess(toPath)), so the reverse rename is allowed back out of a project it landed in; the main-process write suite now asserts the forward/undo/redo moves, and a new renderer suite pins the exact rename calls. - Preserved user-named status across a move —
getUpdatedRelativePathshort-circuits whenrelativePath === filePath, so a full-path tab keeps a full path instead of being recomputed project-relative and silently losing itsuser-fileaccess. - Simplified the resolver — removed
resolveDocumentRenameSourceandisSameFolder, foldedresolveLocalRequestback intoresolveLocalRequestPath, un-exported the two IPC-internal error messages, and scopeddocument-foldertoimport-intoonly. - Added a single-chunk fast path in
readLocalFileBounded, avoiding a copy for a one-chunk read.
The widening is not a renderer capability change: the renderer is trusted, user-file already permits arbitrary absolute read/write, a rename is an explicit gesture, and assertNoClobberRenameDestinationAvailable still prevents overwriting the target. The automatic-load kinds (document-resource / chat-image) are untouched, so the network-share and device-path refusals remain.
Re-ran the seven touched suites (199 tests), the remap/move suites (20), and pnpm tc:node + pnpm tc:web; all pass. No open pullfrog threads remain (r3's is resolved).
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
…eads-files-outside-projects # Conflicts: # src/main/ipc/notebook.test.ts # src/main/ipc/notebook.ts # src/main/startup/os-opened-documents.ts # src/main/startup/os-opened-markdown-files.test.ts
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review at bbe1afe, the only commit is the origin/main merge b00383c, which had four conflicts. The PR's own resolution files are byte-identical across the merge, so the access-kind behavior cannot have changed.
- Merged
origin/main— brings in the notebook kernel-start cancellation (#24858) and the OS-opened CSV/TSV documents feature, among the upstream commits onmain. - Composed the
notebook.tsconflict — main'screateSenderScopedRequestCancellationsguard now wraps the branch'sresolveUserNamedRegularFile+realpathcwd, so a kernel still starts in the notebook's real folder and is still cancelled when the notebook closes. - Kept the branch's grant removal in
os-opened-documents.ts— main's renamed and generalized resolver no longer calls the deletedauthorizeExternalPath; the floating tab reads each OS-opened document as a user-named file. - Realigned the renamed tests — the deleted
os-opened-markdown-files.test.tsis replaced by main's split suites,os-opened-document-resolution.test.tsdrops itsauthorizeExternalPathmocks and assertions, and theclipboard-image-temp-file.test.tssource ratchet now nameslib/open-document-in-floating-workspace.ts.
Verified pnpm tc:node and the conflicted plus ratchet suites (149 tests) green. No open pullfrog threads remain.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
Review summaryThis is the final readiness and QA pass for this PR. It covers the code at head The problemIf you opened a file that lives outside every project (for example The cause: permission for those files was granted once, in memory, when you opened them, and nothing granted it again when the tab came back after a restart. What changes for you
How it works: the in-memory permission system is gone. Every file request from the window now says what kind of access it needs, and the main process checks that on every request, so there is nothing to lose at restart. Fixed during this final pass
Known gaps, all the same on main (follow-ups, not fixed here)Live testing found three gaps that this PR exposes but doesn't cause. Each is listed in the PR body.
Other smaller items:
What was verified
What wasn't verified
Decisions for you
|
…astes Main's #24489 deletes the in-memory path grant system: every desktop file request now declares its access kind, and the composer preview reads a paste as a chat image. The paste code this branch carries from #24905 still granted its files, so: - a composer paste is written without a grant, and a restored paste is kept only when its real path is a file inside the paste folder, as before, with no grant for either spelling; - the tests keep every containment case and drop the grant assertions; a restore still never makes an outside file readable. The composer attachment cache keeps this branch's draft-store reader.
Main's #24489 removed the in-memory path grants (authorizeExternalPath and fs:authorizeExternalPath): a chat image is now read with chat-image file access, which needs no grant. The paste code is resolved onto that: - a composer paste is written into the paste folder with no grant; - the restore call no longer grants anything. It still reports a paste as kept only when its real path is a file inside Orca's paste folder, and now also only when the stored spelling names that same file (the preview and the send read by that spelling). Anything else is a placeholder.
…5357) #25176 and #24489 landed together and collided in insertRichMarkdownImageFromPath. #25176 added three test files whose @/store mock returns { settings, folderWorkspaces, worktreesByRepo }. #24489 made the same function look up the open document with state.openFiles.find(...) to pass document-folder access to the import. Under the mock, openFiles is undefined, so the lookup throws, the catch shows "Failed to insert image", and no image is inserted -- 38 tests fail on main. The real store always carries openFiles, so production behavior of both PRs is intact; the mocks were just written against the pre-#24489 shape. Add openFiles: [] to the three mocks.
Measured from the post-stage-4 main: 827 commits, 33 conflicting files, 3 of
them in the PTY delivery subsystem. Four analysts planned the clusters
(installer/packaging/docs, main process, sidebar/worktree, app-shell/tabs/
store/i18n); one implementer applied all four, and an independent critic
reviewed the result.
## The two decisions this merge turned on
**orcad must not contain Electron.** This merge's Phase 3 deploys orcad to
every SSH host, and the runtime-electron ratchet had one entry on main:
`src/main/ipc/ui.ts`, pulled into the bundle by `pty/ipc/write-input.ts` for
`isTrustedUIRenderer`. That trust predicate and its id state moved to
`src/main/ipc/trusted-ui-renderer-ids.ts`, whose only Electron import is
`import type { WebContents }`; `ui.ts` re-exports the same five names, so all
15 importers and 35 `vi.mock('./ui')` sites are unchanged. The move is pure:
there is exactly one declaration site for the trust ids. Bundle
`require("electron")` goes 1 -> 0 and the ratchet reports `0 entries`.
**An SFTP preview tab must not resolve to a local file.** Upstream stablyai#24489 made
`editorTabFileAccess` grant a `user-file` entry per open tab. The fork's SFTP
preview tabs carry a REMOTE path in the same field, so the grant would have
named a local path that does not exist, or worse, one that does.
`local-file-access.ts:65-68` now returns `undefined` when `file.sftpTargetId`
is set, which leaves main's default root containment in force — the behaviour
main already has. The fork's two openers are registered in
`USER_NAMED_TAB_OPENERS`. Tests pin both halves: an SFTP tab gets no grant, and
a read-only SSH-workspace tab of the identical shape without `sftpTargetId`
still gets `'user-file'`.
## Defects found that no conflict marker could show
Nine files merged cleanly and were still wrong — the recurring shape of this
sync, now at eighteen instances across five stages:
- `readDirViaSftp` was deleted by upstream with no marker; `sftp-transfer.ts`
and `sftp-download-batch.ts` are migrated to the generator form.
- `clipboard-ipc-handlers.ts` and `use-app-session-persistence.ts` breached
`max-lines` at 303/300 from both sides adding lines, and no cluster owned
them. Resolved by extraction, with no disable: 297 and 271.
- Upstream's new `child-workspaces-toggle-target.ts` consumes the fallback
worktree-visibility builder, which lacked the fork's multi-window filter;
`projectKeysInOtherWindows` is restored there.
- Upstream's new profile-save-delay toast has no timeout and clears only on a
push the main process sends to one window, so it could stick forever in a
fork project window. It is gated to the main window — the same class as the
satellite focus defect fixed in stage 4.
- Upstream's new `vitest-sqlite-database-runtime-files.mjs` allowlist did not
know about the fork's `cache-write-tokens.test.ts`, which opens a real SQLite
fixture.
- Upstream's new runtime member `noteRemoteDesktopHostInput` is called
unguarded, leaving a fork test stub one member short.
- `daemon-host-profile-isolation.test.ts:148` was passing *vacuously*: it
seeded a legacy mirror under a directory the fork's pruner never visits.
`verify-orcinus-identity.mjs` is blind to upstream's second uninstall root
(`daemon-host-profiles`): a resolution keeping upstream's `Orca\...` line passes
the verifier. The kept associations test is the only thing that catches it, so
`orcinus-ci.yml` now runs it, with a pass/fail control recorded.
The Windows CLI launcher crate is unchanged by this merge and still carries
upstream's version resource (`build.rs` `ProductName "Orca"`). Noted, not fixed:
the identity verifier does not read it, and the three pieces move together.
## Review
An independent critic returned ACCEPT with 0 blocking defects, re-measuring
every gate and auditing all 27 decisions on the staged index rather than the
working tree. It found one process defect worth recording: the regression
gate's scope had a hole. CI's own unit list has 12,285 files; the sweep covered
6,650, and 5,635 (46%) were never run — a directory-shaped scope missing about
half the suite for the second stage running. The critic closed it by running
the whole complement on the merge and rechecking its 334 failing files on both
parents: `same=2059 diff=2 mergeOnly=0`, with the merge's totals on that set
equal to upstream's exactly. For stage 6 the scope is `discoverUnitFiles()`
minus nothing, or a reported `|universe - swept|`.
Twelve fork-side test files ship red. Every one is red identically on main and
is fork debt, not merge-caused: stale doubles (the fork's 4-argument
`fastGetViaSftp` against upstream's 3-argument stubs), fixtures missing
upstream's new `muse` provider, and tests pinning upstream identity where the
fork differs by ratified decision. One of them is a known production gap, not
debt: runtime notifier events still reach only the main window, which this
merge adds two more members to; the fork's own comment dates that to the
dungeon-3 re-implant.
Two test mocks were completed so that this merge's own change is covered:
`EditorFileTab.tsx` folds the SFTP icon into `FileIcon` to fit `max-lines` at
392/400, and its only test was red on main for an unrelated missing store key.
8 tests go green; the fix is `unifiedTabsByWorktree: {}` in two mocks.
## Gates
Cold typecheck (after deleting `config/*.tsbuildinfo`): 0 errors, measured
independently three times, the last after the mock fix. oxlint: 0 warnings,
0 errors across 32,832 files; oxfmt clean. verify-orcinus-identity: OK,
`upstreamRefs=0`. Unsigned smoke: OK. Fork contract tests: 32/32. Windows CLI
launcher: the Rust build runs here and passes. Localization catalog: OK. The
five repo ratchets (owner-routing, max-lines, ts-nocheck, process-host-imports,
reliability-gates) all match a parent exactly, with no new bypass.
One merge-only test failure remains, attributed: `mobile-web-source-line-
endings` fails on 871 working-copy files whose index blobs are LF in all three
trees. Exporting the index with `git checkout-index` passes on every tree. They
are stale bytes in this long-lived checkout; `.gitattributes` has pinned
`mobile/**` to `eol=lf` since before this merge, so a fresh checkout — which is
what CI does — is LF.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

ELI5
If you opened a file that isn't inside one of your projects (for example
~/notes.txtfrom the floating workspace), Orca could show it until you restarted. After a restart the tab came back with "Unable to load file — Access denied: path resolves outside allowed directories", and Retry failed the same way every time.Orca used to remember, in memory only, which outside files you had opened, and it often forgot them after a restart. Now every file request says what kind of access it is: "a file the user opened", "a file a document shows", "an image in a chat", "a write beside a document the user opened", or the default, "a file inside a project". Orca's main process checks each kind on the spot and remembers nothing, so a restart can't break a tab any more.
What Changed
The problem
Orca's main process only lets the app touch files inside known folders: your repos, worktrees and folder workspaces. Any other file needed a one-off permission, granted when you opened it (Cmd-click in a terminal, drag-and-drop, typing a path, and so on). Those permissions lived only in memory, so a restart wiped them.
When tabs came back, the editor tried to grant them again, but only for tabs that had saved a full path. A floating-workspace tab saves its path relative to the floating folder (
~by default), so it looked like a project file and was never granted again. Every read was refused, Retry included. Markdown files opened from Finder ("Open With") or the floating markdown picker, andkeybindings.json, had the same problem.Before → after, as you see it
keybindings.json): before, "Access denied" after a restart, and Retry never worked. After, it loads, and saving, autosave and Compare / Keep My Edits on a changed-on-disk banner all work.file://notebook: before, it worked until restart and then showed "Access denied". After, it opens by its full path and keeps working after a restart. These tabs show the full path and have no git-status decoration, because they are no longer project-relative.archive/todo.md), another folder (../other/todo.md) or a project folder. Undo and redo move it back and forth from wherever it went. Undo is Cmd/Ctrl+Z in the file explorer, and it only works if the explorer was already showing that workspace before the rename; a floating-workspace file has no explorer, so its rename can't be undone from the app yet (the same reachability gap project renames have on main). Afterwards the tab keeps its access, so it still saves and can be renamed again, and the path it restores after a restart still gets that access. The same holds for a tab opened by full path in a project workspace (for example a Cmd-click in a project terminal), including when you move the project folder that holds it. The inserted image is saved next to the document.How it works
Each local file request from the desktop window now declares what kind of file access it is, in one optional field. The main process checks each kind and keeps no per-file state.
One rule covers every kind, in the one function that handles them all: a declared kind never refuses what the default project check would allow. Main runs the project check first, and the kind's own rule only adds paths outside your projects. So a kind can't make anything inside a project stricter, and a future kind gets this for free.
shot.pngpointing at~/.ssh/id_rsais refused, and an extensionless link to a real PNG loads.A network-share path outside every project is refused by the project check's path text, before Orca touches the disk or the network, for every kind; the user-opened kind then reads a share the user named, as today. For the document and chat kinds, which load automatically as you scroll, a Windows device path (
\\?\,\\.\) or device name (NUL.png,COM1.jpg) is refused by its text before anything else, even inside a project, and a share is reached only inside a project. Writes beside a document refuse device paths and names outside projects the same way.Other changes:
fs:authorizeExternalPathchannel, and every place that granted a path, including the restore-time re-grant./dev/zero, pipes, sockets, folders). They then read a capped amount from that same open file. Saves refuse to overwrite a device or pipe.Why
What broke first. The first version of this PR let any full path outside a project be read in place, for every caller. Review showed that automatic readers then reached it too, because Markdown preview images and chat images load without a click:
//attacker/share/x.png) would make Orca connect to that host and send the user's Windows login hash./dev/zerowould make the main process buffer data until it ran out of memory.The fix is not to widen reads for everyone, but to let each request say what it is, and to keep the default as strict as before.
Alternative: just re-grant the right tabs at restart. Three rounds of review on that approach each found another reader, or another way of spelling a path, that it missed. It keeps a remembered state that every new feature has to restore correctly. Declaring the access kind on each request leaves nothing to restore.
Differences from the common pattern (a file named by full path is read where it is, a path relative to a project stays inside it, and content-referenced loads are limited to the document's folders):
..keeps those segments in the tab's path, as project renames already do, so opening the same file by its plain path can open a second tab;All are small follow-ups.
Linked Issue
None. Reported directly.
Visual Proof
Live checks ran on a remote Mac (M4Air) and a Windows machine: a hidden development build of origin/main (before) and of this branch at bbe1afe (after), each with its own throwaway home folder and profile. The final head adds only a merge of main on top of that.
A file opened outside every project, after a restart. Before: "Access denied" and Retry fails. After: it loads.
A notebook outside every project, after a restart. After: it loads and its kernel runs (prints 42).
A project file link that points outside the project, after a restart. After: it opens by full path and still loads.
A project folder link that points outside shows a message instead of being followed (main followed it). The same shot shows an image pasted into a markdown file outside every project, saved next to it and shown in the editor.
Rename anywhere: the outside notes.txt renamed into a subfolder, another folder and then into a project, saved there and restored after a restart. The path keeps the typed
..segments (listed under Temporary).On Windows, markdown images pointing at network shares (
//host/share/x.png,file://host/share/x.png) stay broken and Orca's file reads never touch the share, inside or outside a project, and a normal image next to an outside document now shows (on main it didn't). One gap remains and is the same on main: see Known gaps below.Testing
What the tests cover:
/dev/zero,/dev/urandomand a symlink to a device are refused without reading, and saves onto/dev/nullare refused..., a/var-style parent alias, a case variant on a case-insensitive disk), is refused for reads and writes when the request declares nothing. When the user opened it, it reads in place.\\?\and\\.\paths, device names and dot-segment spellings likeNUL.png\.are refused with zero filesystem calls, for automatic loads and for writes beside a document;K, a decomposed accent);profile-state-backup-worker.test.ts. That file and its imports are untouched by this PR.Not verified: a real restart in the app, any run on Windows (Windows behaviour is checked with Windows path rules only), and a manual Linux run (CI covers Linux tests).
AI Disclosure
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Known gaps found in live testing, all the same on main and not changed by this PR (follow-ups):
//host/share/x.png,file://host/share/x.png). When the editor view is torn down, its custom image renderer is removed first and the images are redrawn from the stock markup in the live page, so the browser engine fetches them itself. The preview and Orca's main process are not involved.Known limitation: a markdown file opened from a raw network-share path outside your projects (
\\nas\notes\todo.md) can't show the images beside it. That is the same as before this PR. Mapped drive letters and mounted volumes aren't affected.Security:
SSH: requests with an SSH connection go to the remote host as before. The access field is never sent to SSH or remote servers. SSH-owned tabs never read local disk as user-opened files.
Cross-platform:
O_NONBLOCKdoesn't exist on Windows and is skipped there.Paired mobile and web clients: they use runtime file calls, which this PR doesn't change. They are stricter now, because a desktop permission no longer leaks into their checks. A phone can only read or save tabs that the desktop opened.
Compatibility:
fs:authorizeExternalPathchannel is removed; nothing outside the app used it.Performance:
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred). Node and web typechecks, scoped lint, the anti-slop check and related suites pass locally; CI covers the full runs.