{"record":{"id":"59878ec29538f4e6","repo":"deepset-ai/haystack","slug":"tool-concurrency-limit-must-be-greater-than-or-equ","errorCode":null,"errorMessage":"tool_concurrency_limit must be greater than or equal to 1.","messagePattern":"tool_concurrency_limit must be greater than or equal to 1\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"haystack/components/agents/agent.py","lineNumber":483,"sourceCode":"                f\"{type(chat_generator).__name__} does not accept tools parameter in its run method. \"\n                \"The Agent component requires a chat generator that supports tools when tools are provided.\"\n            )\n\n        if exit_conditions is None:\n            exit_conditions = [\"text\"]\n\n        if state_schema is not None:\n            reserved_keys = _RUN_METADATA_STATE_KEYS.keys() | _INTERNAL_STATE_KEYS.keys()\n            reserved_used = sorted(set(state_schema) & reserved_keys)\n            if reserved_used:\n                raise ValueError(\n                    f\"state_schema keys {reserved_used} are reserved for Agent internal state and \"\n                    f\"cannot be redefined. Reserved keys: {sorted(reserved_keys)}.\"\n                )\n            _validate_schema(state_schema)\n        _validate_prompt_message_blocks(user_prompt, system_prompt)\n        if tool_concurrency_limit < 1:\n            raise ValueError(\"tool_concurrency_limit must be greater than or equal to 1.\")\n\n        hooks = hooks or {}\n        _validate_hooks(hooks)\n\n        # --- Attributes ---\n        self.chat_generator = chat_generator\n        # We use an explicit None check for tools b/c testing for truthiness calls __len__, which for SearchableToolset\n        # would iterate and prematurely warm it up at init.\n        self.tools = tools if tools is not None else []\n        self.system_prompt = system_prompt\n        self.user_prompt = user_prompt\n        self.required_variables = required_variables\n        self.exit_conditions = exit_conditions\n        self.max_agent_steps = max_agent_steps\n        self.raise_on_tool_invocation_failure = raise_on_tool_invocation_failure\n        self.streaming_callback = streaming_callback\n        self.tool_concurrency_limit = tool_concurrency_limit\n        self.tool_streaming_callback_passthrough = tool_streaming_callback_passthrough","sourceCodeStart":465,"sourceCodeEnd":501,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/components/agents/agent.py#L465-L501","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nstore.read_skill_file(\"my-skill\", \"../shared/config.json\")\n// after\nstore.read_skill_file(\"my-skill\", \"config.json\")  # file copied into the skill dir","handlingStrategy":"validation","validationCode":"from pathlib import PurePosixPath\n\ndef safe_relative_path(path: str) -> str | None:\n    p = PurePosixPath(path.lstrip(\"/\"))\n    if p.is_absolute() or \"..\" in p.parts:\n        return None\n    return str(p)\n\npath = safe_relative_path(user_path)\nif path is None:\n    raise ValueError(\"Path must be relative and inside the skill\")","typeGuard":"def is_safe_skill_path(path: str) -> bool:\n    p = PurePosixPath(path)\n    return not p.is_absolute() and \"..\" not in p.parts","tryCatchPattern":"try:\n    content = store.read_skill_file(name, path)\nexcept PermissionError as e:\n    logger.warning(\"Blocked path %r: %s\", path, e)\n    content = None","preventionTips":["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"],"tags":["security","path-traversal","skill-store","permission"],"backgroundTag":"path-traversal-blocked","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}