Fix doubled workspace path when LLM passes absolute-style relative paths
resolve_safe_path now detects when the LLM passes the workspace root as a relative path (e.g. "home/user/project/file.py") and strips the prefix before joining. Also updated the system prompt to explicitly instruct the model to use relative paths for all tool arguments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,10 @@ class AgentLoop:
|
|||||||
"You are SneakyCode, a local AI coding agent. "
|
"You are SneakyCode, a local AI coding agent. "
|
||||||
"You help users with software engineering tasks by reading files, "
|
"You help users with software engineering tasks by reading files, "
|
||||||
"searching code, and answering questions about their project.\n\n"
|
"searching code, and answering questions about their project.\n\n"
|
||||||
f"Workspace root: {self._config.agent.workspace_root}\n\n"
|
f"Workspace root: {self._config.agent.workspace_root}\n"
|
||||||
|
"IMPORTANT: All tool path arguments must be RELATIVE to the workspace root. "
|
||||||
|
'Use "." for the root, "app/main.py" for files, "app/" for subdirectories. '
|
||||||
|
"Never pass absolute paths or the workspace root path itself.\n\n"
|
||||||
"Available tools: " + ", ".join(tool_names) + "\n\n"
|
"Available tools: " + ", ".join(tool_names) + "\n\n"
|
||||||
"When you have fully completed the user's request, call the `finish` tool "
|
"When you have fully completed the user's request, call the `finish` tool "
|
||||||
"with a brief summary. If you can answer directly without tools, just respond "
|
"with a brief summary. If you can answer directly without tools, just respond "
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ class BinaryFileError(Exception):
|
|||||||
def resolve_safe_path(path: str | Path, workspace_root: Path) -> Path:
|
def resolve_safe_path(path: str | Path, workspace_root: Path) -> Path:
|
||||||
"""Resolve a path and verify it is within the workspace root.
|
"""Resolve a path and verify it is within the workspace root.
|
||||||
|
|
||||||
|
Handles absolute paths that point within the workspace (e.g. the LLM
|
||||||
|
passing the full workspace path) and relative paths alike.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
path: The path to resolve (absolute or relative to workspace_root).
|
path: The path to resolve (absolute or relative to workspace_root).
|
||||||
workspace_root: The allowed root directory (must be absolute).
|
workspace_root: The allowed root directory (must be absolute).
|
||||||
@@ -29,7 +32,22 @@ def resolve_safe_path(path: str | Path, workspace_root: Path) -> Path:
|
|||||||
PathSecurityError: If the resolved path is outside workspace_root.
|
PathSecurityError: If the resolved path is outside workspace_root.
|
||||||
"""
|
"""
|
||||||
workspace_root = workspace_root.resolve()
|
workspace_root = workspace_root.resolve()
|
||||||
resolved = (workspace_root / path).resolve()
|
path = Path(path)
|
||||||
|
|
||||||
|
# If the path is absolute and within the workspace, use it directly.
|
||||||
|
# If absolute but outside, it will fail the security check below.
|
||||||
|
if path.is_absolute():
|
||||||
|
resolved = path.resolve()
|
||||||
|
else:
|
||||||
|
# Strip workspace_root prefix if the LLM included it as a relative path
|
||||||
|
# e.g. "home/user/project/foo.py" when workspace is "/home/user/project"
|
||||||
|
path_str = str(path)
|
||||||
|
ws_str = str(workspace_root).lstrip("/")
|
||||||
|
if path_str.startswith(ws_str + "/"):
|
||||||
|
path_str = path_str[len(ws_str) + 1:]
|
||||||
|
elif path_str == ws_str:
|
||||||
|
path_str = "."
|
||||||
|
resolved = (workspace_root / path_str).resolve()
|
||||||
|
|
||||||
if not resolved.is_relative_to(workspace_root):
|
if not resolved.is_relative_to(workspace_root):
|
||||||
raise PathSecurityError(
|
raise PathSecurityError(
|
||||||
|
|||||||
Reference in New Issue
Block a user