{"record":{"id":"2e47159aef4e23b7","repo":"langchain-ai/deepagents","slug":"allow-fs-tools-must-be-none-or-a-non-empty-list","errorCode":null,"errorMessage":"allow_fs_tools must be None or a non-empty list","messagePattern":"allow_fs_tools must be None or a non-empty list","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"libs/code/deepagents_code/_server_config.py","lineNumber":494,"sourceCode":"                `allow_fs_tools` is an empty list or omits `\"read_file\"`, or\n                `rubric_max_iterations` / `recursion_limit` is non-positive.\n        \"\"\"\n        if self.sandbox_type == \"none\":\n            object.__setattr__(self, \"sandbox_type\", None)\n        if self.shell_allow_list is not None and len(self.shell_allow_list) == 0:\n            msg = \"shell_allow_list must be None or non-empty\"\n            raise ValueError(msg)\n        # `allow_fs_tools` is a security control: `None` means unrestricted, but\n        # an explicit list must be a usable allowlist. Own the non-empty +\n        # `read_file`-required invariant here (the single authoritative point\n        # for both the env round-trip via `from_env` and direct construction)\n        # rather than deferring to `FilesystemMiddleware`, which would only\n        # surface the violation a process boundary away. `_parse_allow_fs_tools_flag`\n        # still enforces the same rule at the CLI for a friendlier error.\n        if self.allow_fs_tools is not None:\n            if len(self.allow_fs_tools) == 0:\n                msg = \"allow_fs_tools must be None or a non-empty list\"\n                raise ValueError(msg)\n            if \"read_file\" not in self.allow_fs_tools:\n                msg = \"allow_fs_tools must include 'read_file'\"\n                raise ValueError(msg)\n        if isinstance(self.rubric_max_iterations, bool):\n            msg = \"rubric_max_iterations must be None or a positive integer\"\n            raise TypeError(msg)\n        if self.rubric_max_iterations is not None and self.rubric_max_iterations <= 0:\n            msg = \"rubric_max_iterations must be None or a positive integer\"\n            raise ValueError(msg)\n        if isinstance(self.cli_max_retries, bool):\n            msg = \"cli_max_retries must be None or a non-negative integer\"\n            raise TypeError(msg)\n        if self.cli_max_retries is not None and self.cli_max_retries < 0:\n            msg = \"cli_max_retries must be None or a non-negative integer\"\n            raise ValueError(msg)\n        if isinstance(self.recursion_limit, bool):\n            msg = \"recursion_limit must be None or a positive integer\"\n            raise TypeError(msg)","sourceCodeStart":476,"sourceCodeEnd":512,"githubUrl":"https://github.com/langchain-ai/deepagents/blob/a1af029e6e73cb17c36bff823d227747b28e91e1/libs/code/deepagents_code/_server_config.py#L476-L512","documentation":"`ServerConfig.__post_init__` treats `allow_fs_tools` as a security control: `None` means unrestricted, but an explicit list must be usable as an allowlist — non-empty and containing `read_file` (so the agent can always read files). This `ValueError` fires for an explicitly empty list; the sibling check raises for a list missing `read_file`. It is enforced at the single authoritative construction point so violations surface immediately, not a process boundary away in `FilesystemMiddleware`.","triggerScenarios":"Constructing `ServerConfig` with `allow_fs_tools=[]`, or setting `DEEPAGENTS_CODE_SERVER_ALLOW_FS_TOOLS` to a JSON array that round-trips to an empty list, then calling `from_env`/`from_cli_args`.","commonSituations":"Stripping all tools from a list programmatically and passing the result; an env deploy template rendering an empty array `[]`; code intending 'no restriction' that passes `[]` instead of `None`.","solutions":["Pass `allow_fs_tools=None` to mean unrestricted filesystem tools","Provide a non-empty list that includes \"read_file\", e.g. allow_fs_tools=[\"read_file\",\"write_file\"]","Fix upstream code so an emptied list becomes `None` rather than `[]`","Use the `--allow-fs-tools` CLI flag, which enforces the same rule earlier with a friendlier error (`_parse_allow_fs_tools_flag`)"],"exampleFix":"// before\nconfig = ServerConfig(allow_fs_tools=[], ...)\n# ValueError: allow_fs_tools must be None or a non-empty list\n// after\nconfig = ServerConfig(allow_fs_tools=None, ...)  # or [\"read_file\", \"glob\"]","handlingStrategy":"validation","validationCode":"def normalize_allow_fs_tools(tools: list[str] | None) -> list[str] | None:\n    if not tools:\n        return None  # empty -> unrestricted, avoids the empty-list error\n    if \"read_file\" not in tools:\n        tools = [\"read_file\", *tools]  # read_file is mandatory in any allowlist\n    return tools","typeGuard":"def is_valid_allow_fs_tools(value: object) -> bool:\n    if value is None:\n        return True\n    return (\n        isinstance(value, list)\n        and len(value) > 0\n        and all(isinstance(t, str) for t in value)\n        and \"read_file\" in value\n    )","tryCatchPattern":"try:\n    config = ServerConfig(**kwargs)\nexcept ValueError as e:\n    if \"allow_fs_tools\" in str(e):\n        print(f\"Invalid allow_fs_tools: {e}; pass None or a non-empty list including 'read_file'\")\n        sys.exit(2)\n    raise","preventionTips":["Remember None means unrestricted and [] is never valid for this security control","Always include \"read_file\" in any explicit allowlist (the library requires it)","Prefer the --allow-fs-tools CLI flag, which validates the same rule earlier and more clearly","When building the list programmatically, collapse empty results to None before constructing ServerConfig"],"tags":["configuration","security","validation","allowlist"],"backgroundTag":"empty-allowlist-not-allowed","analyzedSha":"a1af029e6e73cb17c36bff823d227747b28e91e1","analyzedAt":"2026-08-29T11:43:24.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}