{"record":{"id":"cc4b7d9fe6f3254b","repo":"agentscope-ai/agentscope","slug":"skill-path-skill-path-r-resolves-outside-skills","errorCode":null,"errorMessage":"Skill path {skill_path!r} resolves outside skills_dir.","messagePattern":"Skill path (.+?) resolves outside skills_dir\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"src/agentscope/workspace/_local_workspace.py","lineNumber":841,"sourceCode":"            counter = 1\n            while agent_name in existing_agent_names:\n                agent_name = f\"{raw_name} ({counter})\"\n                counter += 1\n\n            # Resolve directory name conflict\n            base_dir = _sanitize_dir_name(raw_name)\n            dir_name = base_dir\n            counter = 1\n            while dir_name in existing_dir_names:\n                dir_name = f\"{base_dir}_{counter}\"\n                counter += 1\n\n            dest_path = os.path.join(skills_dir, dir_name)\n\n            if not os.path.realpath(dest_path).startswith(\n                os.path.realpath(skills_dir) + os.sep,\n            ):\n                raise ValueError(\n                    f\"Skill path {skill_path!r} resolves outside skills_dir.\",\n                )\n\n            await asyncio.to_thread(\n                shutil.copytree,\n                skill_path,\n                dest_path,\n                dirs_exist_ok=False,\n            )\n\n            logger.info(\n                \"Copied skill '%s' (agent name: '%s') from %s to %s\",\n                raw_name,\n                agent_name,\n                skill_path,\n                dest_path,\n            )\n","sourceCodeStart":823,"sourceCodeEnd":859,"githubUrl":"https://github.com/agentscope-ai/agentscope/blob/e90f1c7592896cc95f6e5ee506194f533378247d/src/agentscope/workspace/_local_workspace.py#L823-L859","documentation":"Raised by LocalWorkspace.add_skill when the computed destination path inside skills_dir resolves (via realpath) outside the skills directory itself. This is a path-traversal guard: the skill's directory name, after symlink resolution, must stay within skills_dir + os.sep. It protects the agent's skill partition from escapes via symlinks or crafted directory names.","triggerScenarios":"Calling add_skill with a skill directory whose name resolves oddly (e.g. '..'-containing names), or when skills_dir itself is (or contains) a symlink such that realpath(dest_path) no longer shares the realpath(skills_dir) prefix; also a skills_dir that resolves to the filesystem root.","commonSituations":"Skills directory placed under a symlinked path (e.g. /tmp on macOS symlinked to /private/tmp) where skills_dir passed in is not the realpath, or a skill folder that is itself a symlink pointing elsewhere.","solutions":["Pass the canonical skills_dir: use os.path.realpath on the configured skills directory before constructing the workspace.","Ensure the skill source directory is a real directory, not a symlink pointing outside skills_dir.","Check dir_name derived from the skill for path separators or '..' and rename the skill folder to a plain directory name.","Re-run add_skill after fixing so realpath(dest) starts with realpath(skills_dir) + os.sep."],"exampleFix":"# before\nws = LocalWorkspace(skills_dir=\"/tmp/skills\")  # /tmp -> /private/tmp symlink\nawait ws.add_skill(\"./my-skill\")\n\n# after\nimport os\nws = LocalWorkspace(skills_dir=os.path.realpath(\"/tmp/skills\"))\nawait ws.add_skill(\"./my-skill\")","handlingStrategy":"validation","validationCode":"import os\n\ndef skill_dest_is_safe(skills_dir: str, skill_path: str) -> bool:\n    root = os.path.realpath(skills_dir)\n    dest = os.path.realpath(os.path.join(skills_dir, os.path.basename(skill_path.rstrip(\"/\"))))\n    return dest.startswith(root + os.sep)","typeGuard":null,"tryCatchPattern":"try:\n    await ws.add_skill(path)\nexcept ValueError as e:\n    if \"resolves outside skills_dir\" in str(e):\n        raise ValueError(f\"symlinked skill dir not allowed: {path}\") from e\n    raise","preventionTips":["Canonicalize skills_dir with os.path.realpath before configuring the workspace.","Avoid symlinking individual skill directories; copy them instead.","Keep skill folder names simple identifiers (letters, digits, dashes)."],"tags":["agentscope","path-traversal","symlink","workspace","skill"],"backgroundTag":"path-traversal-blocked","analyzedSha":"e90f1c7592896cc95f6e5ee506194f533378247d","analyzedAt":"2026-08-28T18:24:12.087Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}