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

  1. Ensure SKILL.md exists exactly at the top level of the picked folder with that exact name and path root/SKILL.md.
  2. Check client filters are not dropping SKILL.md from the entries list.
  3. 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

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


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