{"record":{"id":"ab69beb5a24bce11","repo":"agentscope-ai/agentscope","slug":"a-skill-must-be-a-single-folder-got-sorted-roots","errorCode":null,"errorMessage":"A skill must be a single folder, got {sorted(roots)}.","messagePattern":"A skill must be a single folder, got (.+?)\\.","errorType":"exception","errorClass":"SkillUploadError","httpStatus":null,"severity":"error","filePath":"src/agentscope/app/_service/_workspace.py","lineNumber":403,"sourceCode":"                    f\"{entry.path!r} is {entry.size} bytes, over the \"\n                    f\"{MAX_FILE_BYTES}-byte per-file limit.\",\n                )\n            total += entry.size\n\n            parts = entry.path.replace(\"\\\\\", \"/\").split(\"/\")\n            if len(parts) < 2 or any(p in (\"\", \".\", \"..\") for p in parts):\n                raise SkillUploadError(f\"Unsafe upload path: {entry.path!r}\")\n            if entry.path.startswith(\"/\"):\n                raise SkillUploadError(f\"Unsafe upload path: {entry.path!r}\")\n            roots.add(parts[0])\n\n        if total > MAX_TOTAL_BYTES:\n            raise SkillUploadError(\n                f\"The upload is {total} bytes, over the \"\n                f\"{MAX_TOTAL_BYTES}-byte limit.\",\n            )\n        if len(roots) != 1:\n            raise SkillUploadError(\n                f\"A skill must be a single folder, got {sorted(roots)}.\",\n            )\n\n        root = roots.pop()\n        if not any(e.path == f\"{root}/SKILL.md\" for e in manifest.entries):\n            raise SkillUploadError(f\"No SKILL.md at the root of {root!r}.\")\n        return root\n\n    async def install_skill(\n        self,\n        workspace: WorkspaceBase,\n        stream: AsyncIterator[bytes],\n        archive_format: Literal[\"zip\", \"tar\", \"tar.gz\"],\n        name: str,\n        *,\n        agent_id: str | None = None,\n    ) -> None:\n        \"\"\"Pipe a skill archive into a workspace, one install at a time.","sourceCodeStart":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/agentscope-ai/agentscope/blob/e90f1c7592896cc95f6e5ee506194f533378247d/src/agentscope/app/_service/_workspace.py#L385-L421","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before\nall_entries = entries_from_folder_a + entries_from_folder_b\nawait service.upload_skill(ws, UploadManifest(entries=all_entries), stream)\n\n# after\nfor entries in (entries_from_folder_a, entries_from_folder_b):\n    await service.upload_skill(ws, UploadManifest(entries=entries), stream)","handlingStrategy":"validation","validationCode":"roots = {e.path.replace(\"\\\\\", \"/\").split(\"/\")[0] for e in manifest.entries}\nif len(roots) != 1:\n    raise ValueError(f\"Multiple roots {sorted(roots)}; upload one skill per request\")","typeGuard":"def single_root(m: UploadManifest) -> bool:\n    return len({e.path.replace(\"\\\\\", \"/\").split(\"/\")[0] for e in m.entries}) == 1","tryCatchPattern":null,"preventionTips":["One folder pick = one upload request.","Split aggregated drag-and-drop entries by their first path component."],"tags":["upload","single-root","validation","skill"],"backgroundTag":"request-validation-failed","analyzedSha":"e90f1c7592896cc95f6e5ee506194f533378247d","analyzedAt":"2026-08-28T18:24:12.087Z","schemaVersion":2},"datasetVersion":"2026-08-28T21:17:43.275Z"}