agentscope-ai/agentscope · error · SkillUploadError
A skill must be a single folder, got {sorted(roots)}.
Error message
A skill must be a single folder, got {sorted(roots)}. What it means
SkillUploadError raised when the set of first path components (roots) across all entries has more than one element. A skill upload must be a single folder: every entry must live under one root directory, so picking two folders or a directory of folders is rejected.
Source
Thrown at src/agentscope/app/_service/_workspace.py:403
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
async def install_skill(
self,
workspace: WorkspaceBase,
stream: AsyncIterator[bytes],
archive_format: Literal["zip", "tar", "tar.gz"],
name: str,
*,
agent_id: str | None = None,
) -> None:
"""Pipe a skill archive into a workspace, one install at a time.View on GitHub (pinned to e90f1c7592)
Solutions
- Upload one skill folder per request; pick the folder that directly contains SKILL.md.
- Split a multi-root manifest into N single-root manifests and issue N upload_skill calls.
- Fix client code that concatenates entries from different drag-and-drop roots.
Example fix
# before
all_entries = entries_from_folder_a + entries_from_folder_b
await service.upload_skill(ws, UploadManifest(entries=all_entries), stream)
# after
for entries in (entries_from_folder_a, entries_from_folder_b):
await service.upload_skill(ws, UploadManifest(entries=entries), stream) Defensive patterns
Strategy: validation
Validate before calling
roots = {e.path.replace("\\", "/").split("/")[0] for e in manifest.entries}
if len(roots) != 1:
raise ValueError(f"Multiple roots {sorted(roots)}; upload one skill per request") Type guard
def single_root(m: UploadManifest) -> bool:
return len({e.path.replace("\\", "/").split("/")[0] for e in m.entries}) == 1 Prevention
- One folder pick = one upload request.
- Split aggregated drag-and-drop entries by their first path component.
When it happens
Trigger: A manifest mixing paths like "skill-a/SKILL.md" and "skill-b/tool.py" — usually from picking a parent directory containing multiple skills, or from concatenating entries from two separate folder picks into one upload.
Common situations: User selects the workspace folder holding several skills instead of one skill folder; client merges manifests from multiple drag-and-drop folders; batching logic that aggregates unrelated uploads.
Related errors
- The upload contains no files.
- No SKILL.md at the root of {root!r}.
- The manifest lists {len(entries)} files but {file_count} wer
- A skill may hold at most {MAX_FILE_COUNT} files, got {len(en
- {entry.path!r} is {entry.size} bytes, over the {MAX_FILE_BYT
AI-assisted analysis of agentscope-ai/agentscope@e90f1c7592 (2026-08-28).
Data as JSON: /api/errors/ab69beb5a24bce11.
Report an issue: GitHub.