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 rootView on GitHub (pinned to e90f1c7592)
Solutions
- Build entry.path from the file's path relative to the picked folder's parent (like webkitRelativePath), never from basename alone.
- Reject entries containing '..', './', empty segments, or backslashes before submitting.
- 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
- Derive paths from the folder-relative location, never basename.
- Sanitize/collapse '//' and './' before adding entries.
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
- The upload contains no files.
- The manifest lists {len(entries)} files but {file_count} wer
- A skill must be a single folder, got {sorted(roots)}.
- No SKILL.md at the root of {root!r}.
- Invalid blob key: {key!r}
AI-assisted analysis of agentscope-ai/agentscope@e90f1c7592 (2026-08-28).
Data as JSON: /api/errors/fb5fd63bd025335d.
Report an issue: GitHub.