Skip to content

[Bug] The desktop read / write / edit tools resolve their target lexically, so a symlink inside the workspace reaches outside it #526

Open

Description

@modacker

Title

The desktop read / write / edit tools resolve their target lexically, so a symlink inside the workspace reaches outside it

Where

packages/agent-tools/src/desktop/local-pi-tools.ts

resolveLocalCandidatePath → normalizeLocalExistingPath resolves the
target against the workspace root and hands that spelling to pi, which opens
it as-is. The path guard that already knows about symlinks
(packages/agent-tools/src/desktop/path-guard.ts) is not called by these
three tools; it serves local-website-deploy.ts and the cloud
file-transfer.ts.

Reproduction

  1. In the workspace root, create link -> /outside/target.txt.
  2. Ask the agent to read link → the content of /outside/target.txt comes
    back.
  3. Ask it to write to link → /outside/target.txt changes.
  4. Ask it to edit link → /outside/target.txt changes.

A dangling symlink is the sharpest case: link -> /outside/new.txt, where
/outside/new.txt does not exist yet. Writing through it creates the file
outside the workspace.

The cloud path has the same blind spot in
packages/agent-tools/src/cloud/matrix-tools/path-guard.ts, where a dangling
link was treated as a missing file and compared lexically.

Impact

  • the agent reads and writes outside the workspace without any prompt
  • the permission layer compares paths lexically too, so these calls never
    reach the approval prompt
  • a dangling symlink lets a write create a file at an arbitrary path

Expected

The tool resolves the target through its real location — following symlinks,
and following dangling ones by hand — and refuses it when that location is
outside the workspace.

The fence should only validate. pi should still receive the lexical absolute
path, byte for byte, because the ledger key has to stay aligned with pi's own
resolveToCwd output. Changing the spelling would change the ledger key.

Notes and limits

  • the check and the open are two steps, so the link can be swapped in
    between. Closing that race needs a lower-level open.
  • bash is not covered here. It is arbitrary execution by design; a check
    there is advice, not a boundary.
  • a target that is already outside the workspace lexically keeps its current
    approval flow. Turning that approval into a hard failure would break
    cross-workspace reads users approve today, and that belongs to the
    permission layer rather than here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions