agentscope-ai/agentscope · error · SkillUploadError
No SKILL.md at the root of {root!r}.
Error message
No SKILL.md at the root of {root!r}. What it means
SkillUploadError raised at the end of validate_manifest: after establishing the single root folder, it requires an entry whose path is exactly f"{root}/SKILL.md". SKILL.md is the manifest that defines a skill, so an upload without it at the root is not a valid skill.
Source
Thrown at src/agentscope/app/_service/_workspace.py:409
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.
Takes the workspace rather than a session, because the
from-library path resolves once and then installs several.
Args:
workspace (`WorkspaceBase`): The target workspace.View on GitHub (pinned to e90f1c7592)
Solutions
- Ensure SKILL.md exists exactly at the top level of the picked folder with that exact name and path root/SKILL.md.
- Check client filters are not dropping SKILL.md from the entries list.
- If SKILL.md is nested, restructure so the folder containing SKILL.md is the upload root.
Example fix
# before # picking repo/ which contains my-skill/SKILL.md # after # pick repo/my-skill/ so entries include "my-skill/SKILL.md"
Defensive patterns
Strategy: validation
Validate before calling
roots = {e.path.replace("\\", "/").split("/")[0] for e in manifest.entries}
root = next(iter(roots)) if len(roots) == 1 else None
assert root and any(e.path == f"{root}/SKILL.md" for e in manifest.entries), "Missing SKILL.md at root" Type guard
def has_skill_md(m: UploadManifest) -> bool:
roots = {e.path.replace("\\", "/").split("/")[0] for e in m.entries}
return len(roots) == 1 and any(e.path == f"{next(iter(roots))}/SKILL.md" for e in m.entries) Prevention
- Pick the folder that directly contains SKILL.md.
- Use the exact filename SKILL.md and don't filter .md files out of the manifest.
When it happens
Trigger: The picked folder lacks SKILL.md at its top level — e.g. SKILL.md sits in a subfolder, is named skill.md/README.md, or the client filtered it out of the manifest.
Common situations: Wrong folder selected (parent or child of the actual skill root); case or naming mistakes (skill.md, SKILL.MD); .md files excluded by a client-side file-type filter; SKILL.md listed with an absolute or extra-prefixed path so it does not equal root/SKILL.md.
Related errors
- The upload contains no files.
- A skill must be a single folder, got {sorted(roots)}.
- Invalid skill at {skill_path!r}: missing or malformed SKILL.
- The manifest lists {len(entries)} files but {file_count} wer
- A skill may hold at most {MAX_FILE_COUNT} files, got {len(en
AI-assisted analysis of agentscope-ai/agentscope@e90f1c7592 (2026-08-28).
Data as JSON: /api/errors/ab2315d9076efa29.
Report an issue: GitHub.