agentscope-ai/agentscope · error · SkillUploadError

Unsafe upload path: {entry.path!r}

Error message

Unsafe upload path: {entry.path!r}

What it means

SkillUploadError raised when a manifest entry's path, after normalizing backslashes to slashes and splitting, has fewer than 2 parts or contains an empty, '.', or '..' segment. Every path must be 'rootfolder/relative/file' so the skill lands inside a single named root; this check blocks path-traversal and degenerate relative paths.

Source

Thrown at src/agentscope/app/_service/_workspace.py:392

        if len(entries) > MAX_FILE_COUNT:
            raise SkillUploadError(
                f"A skill may hold at most {MAX_FILE_COUNT} files, "
                f"got {len(entries)}.",
            )

        total = 0
        roots: set[str] = set()
        for entry in entries:
            if entry.size > MAX_FILE_BYTES:
                raise SkillUploadError(
                    f"{entry.path!r} is {entry.size} bytes, over the "
                    f"{MAX_FILE_BYTES}-byte per-file limit.",
                )
            total += entry.size

            parts = entry.path.replace("\\", "/").split("/")
            if len(parts) < 2 or any(p in ("", ".", "..") for p in parts):
                raise SkillUploadError(f"Unsafe upload path: {entry.path!r}")
            if entry.path.startswith("/"):
                raise SkillUploadError(f"Unsafe upload path: {entry.path!r}")
            roots.add(parts[0])

        if total > MAX_TOTAL_BYTES:
            raise SkillUploadError(
                f"The upload is {total} bytes, over the "
                f"{MAX_TOTAL_BYTES}-byte limit.",
            )
        if len(roots) != 1:
            raise SkillUploadError(
                f"A skill must be a single folder, got {sorted(roots)}.",
            )

        root = roots.pop()
        if not any(e.path == f"{root}/SKILL.md" for e in manifest.entries):
            raise SkillUploadError(f"No SKILL.md at the root of {root!r}.")
        return root

View on GitHub (pinned to e90f1c7592)

Solutions

  1. Build entry.path from the file's path relative to the picked folder's parent (like webkitRelativePath), never from basename alone.
  2. Reject entries containing '..', './', empty segments, or backslashes before submitting.
  3. If using os.walk-like traversal, ensure the root folder name is the first path component.

Example fix

# before
entries = [UploadEntry(path=f.name, size=f.stat().st_size) for f in root.rglob("*")]

# after
entries = [UploadEntry(path=str(f.relative_to(root.parent)).replace(os.sep, "/"), size=f.stat().st_size) for f in root.rglob("*") if f.is_file()]
Defensive patterns

Strategy: validation

Validate before calling

import re
def safe_path(p: str) -> bool:
    parts = p.replace("\\", "/").split("/")
    return len(parts) >= 2 and not any(x in ("", ".", "..") for x in parts) and not p.startswith("/")

bad = [e.path for e in manifest.entries if not safe_path(e.path)]
assert not bad, bad

Type guard

def safe_path(p: str) -> bool:
    parts = p.replace("\\", "/").split("/")
    return len(parts) >= 2 and not any(x in ("", ".", "..") for x in parts) and not p.startswith("/")

Prevention

When it happens

Trigger: A manifest entry like "SKILL.md" (no root folder), "a//b.txt" or "a/./b" (empty/'.' part), or "pkg/../secret.txt" ('..' traversal). Usually produced by client code that builds paths from file.name instead of webkitRelativePath, or by maliciously crafted manifests.

Common situations: Using bare filenames instead of the folder-relative path from the directory picker; symlinks resolving outside the folder; hand-built manifests; sanitizing code that collapses segments to empty strings.

Related errors


AI-assisted analysis of agentscope-ai/agentscope@e90f1c7592 (2026-08-28). Data as JSON: /api/errors/fb5fd63bd025335d. Report an issue: GitHub.