{"record":{"id":"0166851c459cf6ed","repo":"Panniantong/Agent-Reach","slug":"label-current-016685","errorCode":null,"errorMessage":"{label}不能经过符号链接：{current}","messagePattern":"(.+?)不能经过符号链接：(.+?)","errorType":"exception","errorClass":"PrivatePathError","httpStatus":null,"severity":"error","filePath":"agent_reach/utils/paths.py","lineNumber":46,"sourceCode":"    expanded = os.path.expanduser(\"~\")\n    if expanded and expanded != \"~\":\n        return Path(expanded)\n    return Path.home()\n\n\ndef ensure_no_symlink_path(path: str | Path, label: str = \"路径\") -> Path:\n    \"\"\"Reject any existing symlink component without resolving the path.\"\"\"\n    target = Path(path)\n    absolute = Path(os.path.abspath(os.fspath(target)))\n    current = Path(absolute.anchor)\n    for part in absolute.parts[1:]:\n        current /= part\n        try:\n            mode = os.lstat(current).st_mode\n        except FileNotFoundError:\n            continue\n        if stat.S_ISLNK(mode):\n            raise PrivatePathError(f\"{label}不能经过符号链接：{current}\")\n    return target\n\n\ndef make_private_dir(path: str | Path) -> Path:\n    \"\"\"Create a directory restricted to the current user where supported.\"\"\"\n    target = ensure_no_symlink_path(path, \"私密目录\")\n    target.mkdir(mode=0o700, parents=True, exist_ok=True)\n    ensure_no_symlink_path(target, \"私密目录\")\n    if sys.platform != \"win32\":\n        flags = (\n            os.O_RDONLY\n            | getattr(os, \"O_DIRECTORY\", 0)\n            | getattr(os, \"O_NOFOLLOW\", 0)\n        )\n        dir_fd = os.open(target, flags)\n        try:\n            ensure_no_symlink_path(target, \"私密目录\")\n            if hasattr(os, \"fchmod\"):","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/Panniantong/Agent-Reach/blob/93ae1d18c37b707dec053c7c4f9d91cd8ef8943d/agent_reach/utils/paths.py#L28-L64","documentation":"Raised by ensure_no_symlink_path when any existing component of the path being checked is a symbolic link. Agent Reach hardened its private-directory and file-read paths against symlink traversal attacks (TOCTOU / link-swapping), so private data directories and read targets must not traverse symlinks. The message is in Chinese: '{label} cannot pass through a symlink: {current}'.","triggerScenarios":"Calling make_private_dir() or the private read helper with a path where an intermediate component (e.g. ~/.config being a symlink, common on macOS where ~/Library/... is linked, or a dotfiles-managed symlink) is a link. The final component being a symlink also triggers it since every part is lstat'ed.","commonSituations":"Users who symlink ~/.config to a dotfiles repo; nix/homebrew setups where config dirs are links; XDG_CONFIG_HOME pointing at a symlinked path; test fixtures that create temp dirs via symlinks; macOS where /tmp is a symlink to /private/tmp (use realpath before calling).","solutions":["Resolve symlinks before passing the path: pass Path(path).resolve() so the checked path contains real components","Move the real directory into place instead of symlinking it (e.g. point the dotfiles tool the other way)","Set XDG_CONFIG_HOME to a real (non-symlinked) directory","On macOS, prefer real paths like /private/tmp over /tmp aliases when constructing paths"],"exampleFix":"# before\nfrom agent_reach.utils.paths import make_private_dir\nd = make_private_dir(\"~/.config/agent-reach\")  # ~/.config is a symlink\n\n# after\nfrom pathlib import Path\nd = make_private_dir(Path(\"~/.config/agent-reach\").expanduser().resolve())","handlingStrategy":"validation","validationCode":"import os, stat\nfrom pathlib import Path\n\ndef path_has_symlink(path: str | Path) -> bool:\n    absolute = Path(os.path.abspath(os.fspath(path)))\n    current = Path(absolute.anchor)\n    for part in absolute.parts[1:]:\n        current /= part\n        try:\n            if stat.S_ISLNK(os.lstat(current).st_mode):\n                return True\n        except FileNotFoundError:\n            return False\n    return False\n\n# usage: assert not path_has_symlink(my_dir)","typeGuard":null,"tryCatchPattern":"from agent_reach.utils.paths import PrivatePathError, make_private_dir\ntry:\n    d = make_private_dir(target)\nexcept PrivatePathError:\n    d = make_private_dir(Path(target).resolve())  # retry on the resolved path","preventionTips":["Pre-resolve paths with Path(...).resolve() before passing them into private-dir APIs","Never symlink config dirs; let dotfiles tools create real files or symlinks in the other direction","Set XDG_CONFIG_HOME to a real directory, not a link"],"tags":["filesystem","symlink","security","paths"],"backgroundTag":null,"analyzedSha":"93ae1d18c37b707dec053c7c4f9d91cd8ef8943d","analyzedAt":"2026-08-14T22:54:06.735Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}