langchain-ai / langchain-ai/deepagentsjs

Filesystem permission layer is POSIX-`/`-only: a Windows real working directory (`D:\...`) cannot be expressed as a permission glob, forcing consumers into virtualMode

Open
#634 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
1.6k
Forks
272
Avg merge
1d 13h
Merged PRs (30d)
38

Description

`createFilesystemMiddleware`'s permission layer hard-requires `/`-rooted path strings, and the fs
tools hand one model-supplied path string to BOTH the permission check and the native
`path.resolve`/`fs` backend. On Windows these two requirements are mutually exclusive:

- `validatePath` throws unless the path starts with `/` (`if (!raw.startsWith("/")) throw "path must be absolute"`).
- The native backend needs a platform-native string for `path.resolve`/`fs` (`D:\work\src`).

A real Windows working directory is `D:\work`. As a permission glob (`allow D:\work/**`) it fails
`validatePath`. Rewritten to a `/`-rooted form (`/D:/work/**`) to satisfy `validatePath`, it no
longer resolves to the real directory on the backend side. No single string satisfies both uses, so
a consumer that wants real-path containment on Windows (`allow /**`, `deny /**`) cannot express
it: the middleware throws, at construction on the rule paths and again per turn on model-supplied
paths. The only escape today is `virtualMode` (cwd becomes the virtual root `/`), which works but
gives up the real-path namespace.

## Affected code (1.10.5)

`libs/deepagents/src/permissions/enforce.ts`, the `/`-rooted requirement:

```ts
export function validatePath(raw: string): string {
if (typeof raw !== "string" || raw.length === 0) throw new Error("path must be a non-empty string");
if (!raw.startsWith("/")) throw new Error(`path must be absolute: ${JSON.stringify(raw)}`); // <-- Windows D:\ fails here
const segments = raw.split("/").filter((s) => s.length > 0);
if (segments.includes("..")) throw new Error(`path must not contain "..": ${JSON.stringify(raw)}`);
if (segments.includes("~")) throw new Error(`path must not contain "~": ${JSON.stringify(raw)}`);
return `/${segments.join("/")}`;
}

// Called on every permission RULE path at middleware setup:
export function validatePermissionPaths(permissions: FilesystemPermission[]): void {
for (const permission of permissions) for (const path of permission.paths) validatePath(path);
}
```

`libs/deepagents/src/middleware/fs.ts` runs that at construction, then feeds the SAME raw model
path to both the permission check and the backend:

```ts
// setup: throws if any rule path is not "/"-rooted
if (permissions.length > 0) validatePermissionPaths(permissions);

// per tool call (read_file shown): one string, two consumers
enforcePermission(permissions, "read", input.file_path); // wants "/"-rooted (validatePath + micromatch)
// ... and the backend does path.resolve(input.file_path) / fs.* which wants the native "D:\..." form
```

So the two subsystems that share `input.file_path` have incompatible path-form requirements on
Windows: the permission check wants `/D:/work/src`, the native backend wants `D:\work\src`.

## Reproduction

No Windows host required to hit the rule-path failure: the check is purely string-based. Simulate a
Windows cwd:

```ts
import { createFilesystemMiddleware, FilesystemBackend } from "deepagents";

const cwd = "D:\\work"; // a real Windows working directory
const backend = new FilesystemBackend({ rootDir: cwd, virtualMode: false });
const permissions = [
{ operations: ["read", "write"], paths: [`${cwd}/**`], mode: "allow" }, // "D:\\work/**"
{ operations: ["read", "write"], paths: ["/**"], mode: "deny" },
];

createFilesystemMiddleware({ backend, permissions });
// throws: Error: path must be absolute: "D:\\work/**"
```

**Expected:** a consumer can confine an agent to a real Windows working directory with
`allow /**` + `deny /**`, the same as on POSIX.
**Actual:** `validatePermissionPaths` throws at construction because `D:\work/**` is not `/`-rooted.
Even after rewriting the rule to `/D:/work/**` to pass `validatePath`, model-supplied real paths
(`D:\work\src\x`) fail the same check on each tool call, and rewriting those to `/D:/...` breaks the
backend's `path.resolve`. There is no form that satisfies both.

## Why it matters

deepagents cannot provide real-path filesystem containment on Windows. Consumers are pushed into
`virtualMode`, which is fine for an agent that lives entirely in the virtual namespace, but breaks
down for any consumer that ALSO runs a real-path-aware tool alongside the fs tools (for example a
shell/exec tool that reports real OS paths). That consumer now presents the model with two path
namespaces that only coincide on POSIX:

- fs tools (virtualMode): `/`-rooted, cwd = `/`
- the real-path tool: `C:\Users\...\`

Models conflate the two, reasoning over `/`-rooted paths from the fs tools while the real-path tool
reports `C:\Users\...`, and emitting navigation/path commands that mix the forms. In our consumer
(a CLI that pairs deepagents fs tools with a separate real-cwd shell tool) this is a live
correctness/UX hazard for coding agents on Windows hosts; the virtualMode fallback keeps the tool
functional but cannot remove the split.

## Root cause

One model-supplied path string is consumed by two subsystems with incompatible path-form
requirements:

- the permission check wants a `/`-rooted, `/`-separated string (for `validatePath` and `micromatch`);
- the native backend wants a platform-native string (for `path.resolve`/`fs`).

On POSIX these coincide, so the design holds. On Windows they cannot, so the `/`-only permission
layer forces the virtualMode fallback.

## Proposed fix

Two directions, either of which restores real-path mode on Windows:

**Option 1: make the permission layer path-style-aware.** Normalize any absolute path (POSIX or
Windows) to a canonical `/`-rooted, `/`-separated MATCHING form before `validatePath`/`globMatch`
(for example `D:\work\src` to `/D:/work/src`, drive letter cased consistently since Windows paths
are case-insensitive), and keep the native form for the backend's `path.resolve`/`fs`. Normalize
permission RULE paths the same way at setup so `allow ${cwd}/**` works cross-platform. `micromatch`
then matches the normalized `/`-separated string; backslashes are converted to `/` for matching
only, never for I/O.

**Option 1: decouple the two uses.** Hand `enforcePermission` a path already normalized for
matching, kept separate from the string passed to the backend for resolution, so each side gets the
form it needs rather than reusing `input.file_path` verbatim for both.

This is a portability/correctness gap, not a security vulnerability (unlike #633), so it need not
block that fix.

If accepted upstream, we bump the `deepagents` dep, set our `shouldUseVirtualFs` back to real-path
on Windows, and add a Windows real-path containment test.

Happy to send a PR if the direction is agreeable.

Contributor guide

Open the contributing guide

Research direction

Start with validatePath and validatePermissionPaths in libs/deepagents/src/permissions/enforce.ts, then trace createFilesystemMiddleware in libs/deepagents/src/middleware/fs.ts where the model path reaches both permission enforcement and the backend. Reproduce the Windows-style rule-path failure and add coverage showing that a real Windows cwd can use allow and deny rules without virtualMode.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.