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

  1. Upload one skill folder per request; pick the folder that directly contains SKILL.md.
  2. Split a multi-root manifest into N single-root manifests and issue N upload_skill calls.
  3. 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

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


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