Skip to content

refactor(files): one access resolver for desktop and host file requests - #28196

Closed
brennanb2025 wants to merge 3 commits into
mainfrom
brennanb2025/cvs-s1-access-resolver
Closed

brennanb2025 wants to merge 3 commits into
mainfrom
brennanb2025/cvs-s1-access-resolver

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​253 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​252
Prod 9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​126 $\color{#cf222e}{\Huge{\mathbf{−}}}$​45 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​81

ELI5

Orca checks every file request against a list of folders the request is allowed to touch. Two parts of Orca keep that list separately: the desktop window (your projects plus the app's floating-workspace folder) and the file service a paired computer, phone, or SSH session reaches through Orca's server (your projects only). This PR gives both one shared checker and passes each caller's folder list in. Nothing you see changes.

What Changed

The problem. No bug is fixed here; this is plumbing. Later PRs in this stack let a paired or SSH host open files in its Orca scratch folder (where chats write HTML visuals) by absolute path. That host-side request has to be authorized by the same rules the desktop already uses for files opened by name, chat images, and document images. Today those rules live in one desktop-only resolver whose folder list is hardwired. The host's runtime file code calls the bare project-folder check directly, in five files plus the injected local filesystem provider. Extending the host would have meant a second copy of the resolver.

User-facing change. None. Every caller gets the same folders as before:

  • desktop: registered projects, their worktree folders and folder workspaces, plus the floating-workspace folder;
  • host: registered projects, their worktree folders and folder workspaces. No floating folder.

Mechanism.

  • New src/main/ipc/local-file-root-scope.ts names what a caller may reach. LocalFileRootScope is { extraRoots(): readonly string[]; accessKinds: ReadonlySet<kind> }:

    • extraRoots(): the folders a caller adds beyond the store's registered roots;
    • accessKinds: the declared access kinds the resolver honours outside the roots for that caller.

    It defines two scopes, resolveScopedAuthorizedPath(path, store, scope, options) (the default-root check) and resolveHostAuthorizedPath:

    • DESKTOP_FILE_ROOT_SCOPE: adds the floating folder and honours every current kind;
    • HOST_FILE_ROOT_SCOPE: adds nothing and honours no kind yet.
  • local-file-access-resolution.ts: the resolver is now resolveLocalRequestPathInScope(path, access, store, operation, scope). Both its default check and the chat-image network-share check use the passed scope. A declared kind that the scope does not honour is treated like no declared access: root check only. resolveLocalRequestPath and resolveDesktopAuthorizedPath keep their signatures and pass the desktop scope, so existing desktop callers and tests are untouched.

  • Runtime host side: the local filesystem provider and the runtime file commands now call resolveHostAuthorizedPath instead of the bare check. The commands covered are explorer preview and chunk reads, phone file reads, watcher roots, and no-clobber upload commits.

Stack position. S1 of 14 in the chat-visuals scratch-folder stack. Base main; no parent PR. S3 (host {path, access?} file targets) builds on it. It authorizes those requests through resolveLocalRequestPathInScope with the host scope, and extends that one value: this host's chat-visuals folder as a root, and user-file confined to the host roots. On the host a clicked file never reaches an arbitrary path the way it can on the desktop. Later, S4 adds the chat-visual kind there.

Why

  • One resolver with the root set as a parameter, over a host copy. A copy would drift: symlink, network-share and device-path refusals already took several rounds to get right on the desktop.
  • A named scope object, over a boolean or a caller-specific function. A boolean would hardcode two caller kinds and still leave the folder lists scattered. The object makes "which folders, which access kinds" a single value each side owns, and later PRs extend it without touching the resolver.
  • The host honours no declared kind yet. The desktop's rules for a file opened by name or a chat image reach outside every root. That is right for the person at this desktop, but a remote client must not get it by declaring a kind. Each host kind arrives with its own confined rule in a later PR.
  • The desktop floating folder stays out of the host scope. It is this desktop's own folder. A paired client must never reach it through the host.

Differences from the common pattern:

  • intended: the host scope honours no declared access kind. A host request declaring any kind gets exactly today's project-root check. No host caller declares one today, so nothing changes. S3 adds a confined user-file.
  • intended: the quick-open file listing and filename search (shared by desktop and host) and the source-control AI target check keep calling the plain project-root check. It is the same root set both sides use today, and moving them is not needed for the stack.

Linked Issue

N/A, maintainer stack PR (chat-visuals scratch-folder plan, PR 2 of the signed plan).

Visual Proof

N/A: no visual or interaction change; authorization refactor with identical root sets.

Testing

  • local-file-access-resolution.test.ts and local-file-access-resolution-network-path.test.ts (the existing resolution matrix) pass unchanged.

  • New local-file-root-scope.test.ts (real filesystem):

    • one resolver serves both scopes: projects and folder workspaces are allowed in both, outside paths refused in both, and the floating folder allowed on the desktop only;
    • a declared kind such as user-file reaches outside the roots on the desktop only. On the host every kind (user-file read/write, chat image, document resource, document folder) is refused outside the roots and still allowed inside them;
    • each entry point (desktop check, desktop resolver, host check) gives the same answer as the resolver with its scope;
    • the runtime file provider and the phone file read refuse the floating folder and allow a folder workspace.
  • New local-file-root-scope-windows-path.test.ts: Windows path forms in both scopes, with Node's path module switched to win32 on every host. Covers drive-letter case, forward slashes, .., a folder workspace on another drive with a space in its name, sibling-prefix and cross-drive refusals, and the floating folder (desktop only).

  • orca-runtime-files-terminal-link-host-translation.test.ts: one assertion now states the host scope's empty extra roots, since the mocked check receives them.

  • Every test under src/main/{runtime,ipc,providers,window,media} passes locally: 1774 files, 18129 tests. tsc -p config/tsconfig.node.json, pnpm run typecheck and the changed-code quality gate pass.

  • Platforms: macOS locally; Windows path forms through the win32 path test; CI covers Linux.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: no root set widens. The host scope adds nothing and honours no declared kind. The desktop scope matches the old hardwired list and kinds.
  • SSH and paired hosts: no wire change. The host answers exactly as before, so older clients are unaffected.
  • Folder workspaces: covered in both scopes and tested.
  • Performance: one extra closure call per authorization.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

The desktop IPC resolver hardwired its default-root check (project roots
plus the floating-workspace folder) while the host's runtime file services
called the bare store-root check directly. Name the root set as a
LocalFileRootScope: desktop adds the floating folder, the host adds nothing.
resolveLocalRequestPathInScope takes the scope, the desktop entry points pass
the desktop scope, and every runtime file caller (and the local filesystem
provider it injects) uses resolveHostAuthorizedPath. No behaviour change.
…onours

The host scope inherited the desktop's declared-kind rules, so a host
request declaring user-file would have reached any path. Each scope now
lists the kinds it honours: the desktop all current kinds, the host none
yet. An unhonoured kind gets the root check only.
…aches

The chat-image row used a markdown file that chat-image refuses anyway, so it
could not catch a regression; each row now targets a path the desktop allows
through that kind.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Superseded by the rebuilt six-PR chat scratch stack (#28385 merged, #28389, #28397, #28437, #28394, #28479). This piece is replaced by #28385 (dangling-link write fix) and #28397 (the host serves a chat's scratch folder as a file root); the shared access resolver is no longer needed. Closing.

@brennanb2025
brennanb2025 deleted the brennanb2025/cvs-s1-access-resolver branch October 14, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant