Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion backend/package/yuxi/agents/skills/remote_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from dataclasses import dataclass
from pathlib import Path
from typing import TYPE_CHECKING
from urllib.parse import urlparse

from sqlalchemy.ext.asyncio import AsyncSession

Expand All @@ -19,6 +20,9 @@
ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
CONTROL_SEQUENCE_RE = re.compile(r"\x1B\][^\x07]*(?:\x07|\x1B\\)|\x1B[\(\)][A-Za-z0-9]")
CLI_TIMEOUT_SECONDS = 300
GITHUB_REPO_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
GITHUB_ALLOWED_HOSTS = {"github.com", "www.github.com"}
INVALID_SOURCE_MESSAGE = "source 仅支持 GitHub owner/repo 或 https://github.com/OWNER/REPO(.git) 格式"


@dataclass(slots=True)
Expand All @@ -37,7 +41,23 @@ def _normalize_source(source: str) -> str:
raise ValueError("source 不能为空")
if any(ch in value for ch in ("\n", "\r", "\x00")):
raise ValueError("source 包含非法字符")
return value
if GITHUB_REPO_PATTERN.fullmatch(value):
return value

parsed = urlparse(value)
if parsed.scheme != "https" or parsed.hostname not in GITHUB_ALLOWED_HOSTS:
raise ValueError(INVALID_SOURCE_MESSAGE)
if parsed.username or parsed.password or parsed.port or parsed.query or parsed.fragment:
raise ValueError(INVALID_SOURCE_MESSAGE)

repo_path = parsed.path.strip("/")
if repo_path.endswith(".git"):
repo_path = repo_path[:-4]
if not GITHUB_REPO_PATTERN.fullmatch(repo_path):
raise ValueError(INVALID_SOURCE_MESSAGE)

owner, repo = repo_path.split("/", 1)
return f"https://github.com/{owner}/{repo}"


def _normalize_skill_name(skill: str) -> str:
Expand Down
23 changes: 23 additions & 0 deletions backend/test/unit/agents/skills/test_remote_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@
from yuxi.agents.skills import remote_install as svc


def test_normalize_source_accepts_owner_repo() -> None:
assert svc._normalize_source("anthropics/skills") == "anthropics/skills"


def test_normalize_source_accepts_canonical_github_url() -> None:
assert svc._normalize_source("https://www.github.com/anthropics/skills.git") == "https://github.com/anthropics/skills"


@pytest.mark.parametrize(
"source",
[
"http://127.0.0.1:9/repo.git",
"https://example.com/owner/repo",
"git@github.com:anthropics/skills.git",
"file:///tmp/skills",
"https://github.com/anthropics/skills?tab=readme",
],
)
def test_normalize_source_rejects_non_github_or_unsafe_url(source: str) -> None:
with pytest.raises(ValueError, match="GitHub owner/repo"):
svc._normalize_source(source)


def test_parse_available_skills_from_cli_output() -> None:
output = """
\x1b[38;5;250m███████╗\x1b[0m
Expand Down