fix: enforce exact Dream memory file writes

maintainer edit: Dream write tools used file paths as directory roots, so a missing canonical memory file could be treated as a parent directory. Add exact-file allowlist support and keep skills/ as the only Dream write directory.
This commit is contained in:
chengyongru
2026-06-18 00:03:26 +08:00
committed by Xubin Ren
parent 732992df4f
commit 15f218e918
8 changed files with 129 additions and 15 deletions
+5 -5
View File
@@ -504,7 +504,7 @@ class MemoryStore:
skills_dir.mkdir(parents=True, exist_ok=True)
extra_read = [BUILTIN_SKILLS_DIR] if BUILTIN_SKILLS_DIR.exists() else None
editable_roots = [self.soul_file, self.user_file, skills_dir]
editable_files = [self.memory_file, self.soul_file, self.user_file]
tools.register(ReadFileTool(
workspace=workspace,
@@ -514,14 +514,14 @@ class MemoryStore:
))
tools.register(EditFileTool(
workspace=workspace,
allowed_dir=self.memory_file,
extra_write_allowed_dirs=editable_roots,
allowed_dir=skills_dir,
extra_write_allowed_files=editable_files,
file_states=file_states,
))
tools.register(ApplyPatchTool(
workspace=workspace,
allowed_dir=self.memory_file,
extra_write_allowed_dirs=editable_roots,
allowed_dir=skills_dir,
extra_write_allowed_files=editable_files,
file_states=file_states,
))
tools.register(WriteFileTool(
+8
View File
@@ -47,6 +47,8 @@ class _FsTool(Tool):
extra_allowed_dirs: list[Path] | None = None,
extra_read_allowed_dirs: list[Path] | None = None,
extra_write_allowed_dirs: list[Path] | None = None,
extra_read_allowed_files: list[Path] | None = None,
extra_write_allowed_files: list[Path] | None = None,
file_states: FileStates | None = None,
restrict_to_workspace: bool | None = None,
sandbox_restricts_workspace: bool = False,
@@ -60,6 +62,8 @@ class _FsTool(Tool):
*(extra_read_allowed_dirs or []),
]
self._extra_write_allowed_dirs = list(extra_write_allowed_dirs or [])
self._extra_read_allowed_files = list(extra_read_allowed_files or [])
self._extra_write_allowed_files = list(extra_write_allowed_files or [])
self._extra_allowed_dirs = self._extra_read_allowed_dirs
self._restrict_to_workspace = (
bool(restrict_to_workspace)
@@ -117,6 +121,7 @@ class _FsTool(Tool):
self,
path: str,
extra_allowed_dirs: list[Path] | None,
extra_allowed_files: list[Path] | None,
*,
include_media_dir: bool,
) -> Path:
@@ -130,6 +135,7 @@ class _FsTool(Tool):
access.project_path,
self._effective_allowed_root(access.allowed_root),
extra_allowed_dirs,
extra_allowed_files,
include_media_dir=include_media_dir,
)
@@ -137,6 +143,7 @@ class _FsTool(Tool):
return self._resolve_with_extra(
path,
self._extra_read_allowed_dirs,
self._extra_read_allowed_files,
include_media_dir=True,
)
@@ -144,6 +151,7 @@ class _FsTool(Tool):
return self._resolve_with_extra(
path,
self._extra_write_allowed_dirs,
self._extra_write_allowed_files,
include_media_dir=False,
)
+2
View File
@@ -19,6 +19,7 @@ def resolve_workspace_path(
workspace: Path | None = None,
allowed_dir: Path | None = None,
extra_allowed_dirs: list[Path] | None = None,
extra_allowed_files: list[Path] | None = None,
include_media_dir: bool = True,
) -> Path:
"""Resolve path against workspace and enforce allowed directory containment."""
@@ -29,4 +30,5 @@ def resolve_workspace_path(
workspace=workspace,
allowed_root=allowed_dir,
extra_allowed_roots=extra_roots,
extra_allowed_files=extra_allowed_files,
)
+26 -4
View File
@@ -44,6 +44,22 @@ def is_path_allowed(path: str | Path, roots: Iterable[str | Path]) -> bool:
return any(is_path_within(path, root) for root in roots)
def is_path_exactly_allowed(path: str | Path, files: Iterable[str | Path]) -> bool:
"""Return True when *path* resolves exactly to one of the allowed files."""
try:
resolved_path = Path(path).expanduser().resolve(strict=False)
except (OSError, RuntimeError, TypeError, ValueError):
return False
for file in files:
try:
resolved_file = Path(file).expanduser().resolve(strict=False)
except (OSError, RuntimeError, TypeError, ValueError):
continue
if resolved_path == resolved_file:
return True
return False
def require_path_within(
path: str | Path,
root: str | Path,
@@ -67,17 +83,23 @@ def resolve_allowed_path(
workspace: str | Path | None = None,
allowed_root: str | Path | None = None,
extra_allowed_roots: Iterable[str | Path] | None = None,
extra_allowed_files: Iterable[str | Path] | None = None,
strict: bool = False,
) -> Path:
"""Resolve a path and enforce containment in allowed roots when configured."""
resolved = resolve_path(path, workspace, strict=False)
if allowed_root is None:
files = list(extra_allowed_files or [])
if allowed_root is None and not files:
return resolve_path(path, workspace, strict=strict) if strict else resolved
roots = [allowed_root, *(extra_allowed_roots or [])]
if not is_path_allowed(resolved, roots):
roots = []
if allowed_root is not None:
roots.append(allowed_root)
roots.extend(extra_allowed_roots or [])
if not is_path_allowed(resolved, roots) and not is_path_exactly_allowed(resolved, files):
boundary = Path(allowed_root).expanduser() if allowed_root is not None else "allowed files"
raise WorkspaceBoundaryError(
f"Path {path} is outside allowed directory {Path(allowed_root).expanduser()}"
f"Path {path} is outside allowed directory {boundary}"
+ WORKSPACE_BOUNDARY_NOTE
)
if strict: