{"record":{"id":"bdf05fd6b6cdc59e","repo":"odysseus-dev/odysseus","slug":"only-github-or-skills-sh-urls-are-supported","errorCode":null,"errorMessage":"Only GitHub or skills.sh URLs are supported","messagePattern":"Only GitHub or skills\\.sh URLs are supported","errorType":"validation","errorClass":"SkillImportError","httpStatus":null,"severity":"error","filePath":"services/memory/skill_importer.py","lineNumber":282,"sourceCode":"\ndef parse_skill_source(url: str) -> ResolvedSource:\n    \"\"\"Normalize skills.sh / GitHub web URLs into owner/repo/ref/path.\"\"\"\n    url = (url or \"\").strip()\n    if not url:\n        raise SkillImportError(\"URL is required\")\n\n    # ``urlparse`` only reports an unambiguous scheme when the URL carries the\n    # ``scheme://`` form. Opaque schemes (``mailto:``, ``javascript:``) and a\n    # schemeless ``host:port`` both parse a \"scheme\" that is not one, so they\n    # fall through to the host check below and are rejected on the host instead.\n    scheme = urlparse(url).scheme.lower()\n    if scheme not in (\"http\", \"https\"):\n        if scheme and url.lower().startswith(f\"{scheme}://\"):\n            raise SkillImportError(f\"unsupported URL scheme: {scheme}\")\n        # Schemeless \"github.com/owner/repo\" — accept only a supported host.\n        rough_host = (urlparse(\"//\" + url).hostname or \"\").lower()\n        if rough_host not in _GITHUB_HOSTS and rough_host not in _SKILLS_SH_HOSTS:\n            raise SkillImportError(\"Only GitHub or skills.sh URLs are supported\")\n        url = \"https://\" + url\n\n    parsed = urlparse(url)\n    hostname = (parsed.hostname or \"\").lower()\n    if hostname not in _GITHUB_HOSTS and hostname not in _SKILLS_SH_HOSTS:\n        raise SkillImportError(\"Only GitHub or skills.sh URLs are supported\")\n\n    # A skills.sh link is only usable if it redirects to an exact supported\n    # GitHub host. Scraping the page body for a github.com link cannot work:\n    # skill pages only ever link the repository root, never the skill's\n    # subdirectory, so the scrape resolves every skill in a repo to the same\n    # (wrong) bundle. Fail with an actionable message instead.\n    if hostname in _SKILLS_SH_HOSTS:\n        r = _get_checked(url, timeout=20.0)\n        if r.status_code >= 400:\n            raise _github_response_error(r)\n        final = str(r.url)\n        if _github_host(final) not in _GITHUB_HOSTS:","sourceCodeStart":264,"sourceCodeEnd":300,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/services/memory/skill_importer.py#L264-L300","documentation":"First host check in parse_skill_source (services/memory/skill_importer.py): for a schemeless URL like 'github.com/owner/repo', the code prepends '//' to extract a rough hostname, and if that host is neither a GitHub host nor a skills.sh host it rejects before upgrading the URL to https. This catches pasted bare domains that are not supported sources, at a point where no scheme context exists yet.","triggerScenarios":"Pasting 'gitlab.com/user/repo', 'bitbucket.org/team/repo', 'example.com/skill', or bare text that happens to parse with a hostname ('notahost/foo'); also schemeless strings whose first segment looks like host:port and parses differently than intended.","commonSituations":"Users pasting a GitLab/Bitbucket URL without the scheme; pasting a skill's homepage rather than its repo; drag-pasting text with a leading token that urlparse treats as a host.","solutions":["Paste a full URL on a supported host: https://github.com/<owner>/<repo>/... or https://skills.sh/...","Re-copy the link from the browser address bar of the GitHub repo/skill folder so the scheme is included.","If the skill lives on GitLab/Bitbucket, it must be mirrored to GitHub before import — those hosts are unsupported by design.","Strip surrounding text/quotes from the pasted value before submitting."],"exampleFix":"# before\nimport_skill(\"gitlab.com/user/repo/skills/foo\")\nSkillImportError: Only GitHub or skills.sh URLs are supported\n\n# after\nimport_skill(\"https://github.com/user/repo/tree/main/skills/foo\")","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\n\n_GITHUB = {'github.com', 'www.github.com', 'api.github.com', 'raw.githubusercontent.com'}\n_SKILLS = {'skills.sh', 'www.skills.sh'}\n\ndef schemeless_host_ok(url: str) -> bool:\n    host = (urlparse('//' + url.strip()).hostname or '').lower()\n    return host in _GITHUB or host in _SKILLS","typeGuard":"def is_supported_source(url: str) -> bool:\n    u = url.strip()\n    host = (urlparse('//' + u).hostname or '').lower() if urlparse(u).scheme not in ('http', 'https') else (urlparse(u).hostname or '').lower()\n    return host in _GITHUB or host in _SKILLS","tryCatchPattern":"try:\n    src = parse_skill_source(url)\nexcept SkillImportError as e:\n    if 'Only GitHub or skills.sh' in str(e):\n        return bad_request('Unsupported source. Paste a github.com or skills.sh skill URL.')\n    raise","preventionTips":["Copy URLs from the browser address bar (includes scheme, avoids the schemeless path entirely).","Client-side hostname validation before submission catches forge links early.","Mirror non-GitHub skills to GitHub instead of trying to make the importer accept other hosts."],"tags":["url-validation","user-input","skill-import","host-allowlist"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}