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
- 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
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