Skip to content
Merged
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
6 changes: 4 additions & 2 deletions ymir/agents/prompts/triage/prompt.j2
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ Goal: Analyze the given issue to determine the correct course of action.
* Determine the name of the package from the issue details (usually component name)
{% if needs_internal_fix and internal_target_branch %}
* Use the clone_repository tool to clone `https://gitlab.com/redhat/rhel/rpms/<package_name>`
with branch `{{ internal_target_branch }}`.
with branch `{{ internal_target_branch }}` and
clone_path `/git-repos/{{ issue }}/<package_name>`.
If the clone fails (branch or repo not found), fall back to cloning
`https://gitlab.com/redhat/centos-stream/rpms/<package_name>` using the same tool.
`https://gitlab.com/redhat/centos-stream/rpms/<package_name>` using the same tool
(keep the same clone_path).
{% else %}
* Confirm the package repository exists by running
`GIT_TERMINAL_PROMPT=0 git ls-remote https://gitlab.com/redhat/centos-stream/rpms/<package_name>`
Expand Down
2 changes: 2 additions & 0 deletions ymir/agents/tests/unit/test_jinja2_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ def test_renders_needs_internal_fix(self):
assert "redhat/rhel/rpms" in result
assert "rhel-10.2" in result
assert "centos-stream" in result
assert "clone_path" in result
assert "/git-repos/RHEL-189361/" in result

def test_renders_needs_internal_fix_without_branch_falls_back(self):
result = render_template(
Expand Down
11 changes: 10 additions & 1 deletion ymir/tools/privileged/gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,9 @@ async def _run(
class CloneRepositoryToolInput(BaseModel):
repository: str = Field(description="Repository to clone")
branch: str | None = Field(default=None, description="Branch to clone. If omitted, all refs are fetched.")
clone_path: AbsolutePath = Field(description="Absolute path where to clone the repository")
clone_path: AbsolutePath = Field(
description="Absolute path under the shared /git-repos volume where to clone the repository"
)


class CloneRepositoryTool(Tool[CloneRepositoryToolInput, ToolRunOptions, StringToolOutput]):
Expand Down Expand Up @@ -460,6 +462,13 @@ async def _run(
repository = tool_input.repository
branch = tool_input.branch
clone_path = tool_input.clone_path

basepath = Path(os.getenv("GIT_REPO_BASEPATH", "/git-repos")).resolve()
resolved = clone_path.resolve()
if resolved == basepath or not resolved.is_relative_to(basepath):
raise ToolError(f"clone_path must be under {basepath} (the shared volume). Got: {clone_path}")
clone_path = resolved
Comment thread
qodo-for-packit[bot] marked this conversation as resolved.

await clean_stale_repositories()

auth_args = _get_git_auth_args(repository)
Expand Down
57 changes: 57 additions & 0 deletions ymir/tools/privileged/tests/unit/test_gitlab.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import gitlab
import pytest
from beeai_framework.tools import ToolError
from flexmock import flexmock
from ogr.abstract import PRStatus
from ogr.exceptions import GitlabAPIException
Expand Down Expand Up @@ -229,6 +230,62 @@ async def wait():
assert result.startswith("Successfully")


@pytest.mark.asyncio
@pytest.mark.parametrize(
"bad_path",
[
Path("/tmp/bash"),
Path("/var/lib/bash"),
],
ids=["outside-base", "unrelated-absolute"],
)
async def test_clone_repository_rejects_path_outside_basepath(mock_git_repo_basepath, bad_path):
with pytest.raises(ToolError, match="must be under"):
await CloneRepositoryTool().run(
input={"repository": "https://gitlab.com/redhat/rhel/rpms/bash", "clone_path": bad_path}
)


@pytest.mark.asyncio
async def test_clone_repository_rejects_path_traversal(mock_git_repo_basepath):
traversal_path = mock_git_repo_basepath / ".." / "tmp" / "bash"
with pytest.raises(ToolError, match="must be under"):
await CloneRepositoryTool().run(
input={"repository": "https://gitlab.com/redhat/rhel/rpms/bash", "clone_path": traversal_path}
)
Comment thread
opohorel marked this conversation as resolved.


@pytest.mark.asyncio
async def test_clone_repository_rejects_basepath_root(mock_git_repo_basepath):
with pytest.raises(ToolError, match="must be under"):
await CloneRepositoryTool().run(
input={
"repository": "https://gitlab.com/redhat/rhel/rpms/bash",
"clone_path": mock_git_repo_basepath,
}
)


@pytest.mark.asyncio
async def test_clone_repository_accepts_path_inside_basepath(mock_git_repo_basepath):
valid_path = mock_git_repo_basepath / "RHEL-12345" / "bash"

async def create_subprocess_exec(cmd, *args, **kwargs):
async def wait():
return 0

return flexmock(wait=wait)

flexmock(asyncio).should_receive("create_subprocess_exec").replace_with(create_subprocess_exec)

result = (
await CloneRepositoryTool().run(
input={"repository": "https://gitlab.com/redhat/rhel/rpms/bash", "clone_path": valid_path}
)
).result
assert result.startswith("Successfully")


@pytest.mark.asyncio
async def test_push_to_remote_repository():
repository = "https://gitlab.com/ai-bot/bash.git"
Expand Down
Loading