Skip to content

feat(security): introduce WorkspaceFileAccess — central authorization layer for all file operations #390

@edelauna

Description

@edelauna

Part of #169. Sub-issue 2 of 4. Depends on #389.

Context

Once WorkspacePathResolver (#389) provides safe async path canonicalization, we need a policy layer that tools can call instead of performing raw fs operations after a boolean check. The current pattern — tool calls isPathOutsideWorkspace(), gets a boolean, then proceeds with its own fs read/write — is structurally easy to get wrong: the check and the operation are decoupled, so one missed call site (like ApplyDiffTool in PR #241) leaves a hole.

The goal of this layer is to make the boundary check structurally impossible to bypass: tools request an authorized operation and get back either a resolved path they are permitted to use, or a structured error they must surface.

Developer Note

Create src/core/workspace/WorkspaceFileAccess.ts (or equivalent location consistent with project conventions).

authorizeRead(options): Promise<AuthorizeResult>

type AuthorizeResult =
  | { ok: true; resolvedPath: string }
  | { ok: false; reason: "outside_workspace" | "symlink_escapes_workspace" | "realpath_failed" | "permission_denied"; message: string }

Options:

  • task: Task — for reading allowSymlinksOutsideWorkspace from provider state (default false when state is unavailable — fail closed).
  • requestedPath: string — absolute path as supplied by the tool.
  • source: string — tool name, for error messages.

Internally:

  1. Call resolveRealPath from feat(security): introduce WorkspacePathResolver — safe async symlink-aware path canonicalization #389. On error, return { ok: false, reason: "realpath_failed" | "permission_denied", message }.
  2. Compare resolved path against each workspace folder (also resolved via resolveRealPath). On error resolving a folder, skip it (cannot prove containment).
  3. If outside all workspace folders and allowSymlinksOutsideWorkspace is false, return { ok: false, reason: … }.
  4. If allowSymlinksOutsideWorkspace is true, compare lexical paths (no realpath) — restoring pre-fix behavior for users who opt in.

Expose authorizeWrite with the same signature for write operations. (Both may share an internal authorize helper.)

Read allowSymlinksOutsideWorkspace from task.providerRef.deref()?.getState() with a try/catch — provider may have been torn down. Default to false.

Tests (src/core/workspace/__tests__/WorkspaceFileAccess.spec.ts):

  • Symlink inside workspace → outside: ok: false, reason: "symlink_escapes_workspace".
  • allowSymlinksOutsideWorkspace: true → symlink inside workspace → outside: ok: true.
  • EACCES on symlink target: ok: false, reason: "permission_denied".
  • Real file inside workspace: ok: true, resolvedPath matches canonical path.
  • Not-yet-created file under symlinked ancestor: ok: true (creation flows still work).
  • Provider state unavailable (deref returns undefined): defaults to fail-closed.

Does not change any tool behavior. No UI, no i18n, no tool migrations in this PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions