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_passthrough

View on GitHub (pinned to e318778c9b)

Solutions

  1. Pass a path relative to the skill root (see the 'Readable files' list in the message) with no '..' components.
  2. Strip leading '/' and normalize with pathlib before calling, e.g. PurePosixPath(path.lstrip('/')).
  3. 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

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


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/59878ec29538f4e6. Report an issue: GitHub.