deepset-ai/haystack · error · ValueError
tool_concurrency_limit must be greater than or equal to 1.
Error message
tool_concurrency_limit must be greater than or equal to 1.
What it means
read_skill_file resolves the requested path against the skill directory and rejects any target that escapes it (path traversal protection). This PermissionError is raised when the resolved path is not the skill dir itself nor inside it — e.g. '../' sequences, absolute paths, or symlinked targets pointing outside — preventing reads of arbitrary files on disk.
Source
Thrown at haystack/components/agents/agent.py:483
f"{type(chat_generator).__name__} does not accept tools parameter in its run method. "
"The Agent component requires a chat generator that supports tools when tools are provided."
)
if exit_conditions is None:
exit_conditions = ["text"]
if state_schema is not None:
reserved_keys = _RUN_METADATA_STATE_KEYS.keys() | _INTERNAL_STATE_KEYS.keys()
reserved_used = sorted(set(state_schema) & reserved_keys)
if reserved_used:
raise ValueError(
f"state_schema keys {reserved_used} are reserved for Agent internal state and "
f"cannot be redefined. Reserved keys: {sorted(reserved_keys)}."
)
_validate_schema(state_schema)
_validate_prompt_message_blocks(user_prompt, system_prompt)
if tool_concurrency_limit < 1:
raise ValueError("tool_concurrency_limit must be greater than or equal to 1.")
hooks = hooks or {}
_validate_hooks(hooks)
# --- Attributes ---
self.chat_generator = chat_generator
# We use an explicit None check for tools b/c testing for truthiness calls __len__, which for SearchableToolset
# would iterate and prematurely warm it up at init.
self.tools = tools if tools is not None else []
self.system_prompt = system_prompt
self.user_prompt = user_prompt
self.required_variables = required_variables
self.exit_conditions = exit_conditions
self.max_agent_steps = max_agent_steps
self.raise_on_tool_invocation_failure = raise_on_tool_invocation_failure
self.streaming_callback = streaming_callback
self.tool_concurrency_limit = tool_concurrency_limit
self.tool_streaming_callback_passthrough = tool_streaming_callback_passthroughView on GitHub (pinned to e318778c9b)
Solutions
- Pass a path relative to the skill root (see the 'Readable files' list in the message) with no '..' components.
- Strip leading '/' and normalize with pathlib before calling, e.g. PurePosixPath(path.lstrip('/')).
- If the file lives outside the skill dir, move/copy it into the skill directory instead of symlinking or traversing.
Example fix
// before
store.read_skill_file("my-skill", "../shared/config.json")
// after
store.read_skill_file("my-skill", "config.json") # file copied into the skill dir Defensive patterns
Strategy: validation
Validate before calling
from pathlib import PurePosixPath
def safe_relative_path(path: str) -> str | None:
p = PurePosixPath(path.lstrip("/"))
if p.is_absolute() or ".." in p.parts:
return None
return str(p)
path = safe_relative_path(user_path)
if path is None:
raise ValueError("Path must be relative and inside the skill") Type guard
def is_safe_skill_path(path: str) -> bool:
p = PurePosixPath(path)
return not p.is_absolute() and ".." not in p.parts Try / catch
try:
content = store.read_skill_file(name, path)
except PermissionError as e:
logger.warning("Blocked path %r: %s", path, e)
content = None Prevention
- Never build skill file paths from raw user/LLM input without stripping '..' and leading '/'
- Keep all assets physically inside the skill directory rather than symlinked outside
- Normalize paths with pathlib before passing them to read_skill_file
- Show callers the list of readable files instead of free-form path entry
When it happens
Trigger: store.read_skill_file('my-skill', '../secret.txt'), an absolute path like '/etc/passwd' (skill_dir / abs collapses to abs), or 'a/../../outside.txt'; also files inside the skill dir that are symlinks to outside locations.
Common situations: LLM agent constructing paths with '..' to navigate; joining user-supplied relative paths naively; a skill containing symlinks to shared assets outside its directory; passing OS-absolute paths instead of skill-relative ones.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Document with ID '{doc.id}' has a file path '{file_path}' th
- {subject} '{path_like}' resolves outside the store root '{ro
- Invalid hook point '{hook_point}'. Valid hook points are: {'
- Hook registered for hook point '{hook_point}' is callable bu
- Hook registered for hook point '{hook_point}' must have a ca
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/59878ec29538f4e6.
Report an issue: GitHub.