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
2 changes: 1 addition & 1 deletion backend/app/repositories/agent_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,7 @@ async def recount_agent_projects(self, agent_id) -> None:

async def get_agent_project_ids(self, agent_id, limit: int) -> list[dict]:
result = await self.db.execute(
text("SELECT id, title FROM projects WHERE creator_agent_id = :aid ORDER BY created_at DESC LIMIT :limit"),
text("SELECT id, title FROM projects WHERE creator_agent_id = :aid AND repo_url IS NOT NULL AND repo_url <> '' ORDER BY created_at DESC LIMIT :limit"),
{"aid": agent_id, "limit": limit},
)
return [dict(r) for r in result.mappings()]
Expand Down
55 changes: 25 additions & 30 deletions backend/app/services/agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,19 @@ async def cancel_notification_tasks(self, source_key: str) -> None:

# ── Projects ──────────────────────────────────────────────────────

@staticmethod
def _require_repository(project: dict) -> dict:
if not project["repo_url"]:
raise HTTPException(status_code=400, detail="Project has no repository")
return project

async def _get_repository_project(self, project_id: UUID, fields: str) -> dict:
project = await self.repo.get_project_basic(project_id, fields)
if not project:
raise HTTPException(status_code=404, detail="Project not found")
self._require_repository(project)
return project

async def create_project(self, agent: dict, body) -> ProjectResponse:
"""Create a new project with git repo, README, token, and contributor setup."""

Expand Down Expand Up @@ -1233,9 +1246,7 @@ async def merge_project_pr(self, project_id: UUID, agent: dict, body: dict) -> d
if not pr_number or not isinstance(pr_number, int):
raise HTTPException(status_code=422, detail="pr_number (int) is required")

project = await self.repo.get_project_basic(project_id, "title, creator_agent_id, vcs_provider")
if not project:
raise HTTPException(status_code=404, detail="Project not found")
project = await self._get_repository_project(project_id, "title, creator_agent_id, vcs_provider, repo_url")

if str(project["creator_agent_id"]) != str(agent["id"]):
raise HTTPException(status_code=403, detail="Only project creator can merge PRs")
Expand Down Expand Up @@ -1280,9 +1291,7 @@ async def list_my_issues(self, agent: dict, state: str, limit: int) -> dict:
async def list_issue_comments(self, project_id: UUID, issue_number: int) -> dict:
"""List comments for a specific issue."""

project = await self.repo.get_project_basic(project_id, "title")
if not project:
raise HTTPException(status_code=404, detail="Project not found")
project = await self._get_repository_project(project_id, "title, repo_url")

comments = await self.git.list_issue_comments(project["title"], issue_number)
return {
Expand All @@ -1295,9 +1304,7 @@ async def list_issue_comments(self, project_id: UUID, issue_number: int) -> dict
async def post_issue_comment(self, project_id: UUID, issue_number: int, body_text: str, agent: dict) -> dict:
"""Post a comment on a GitHub/GitLab issue."""

project = await self.repo.get_project_basic(project_id, "title, repo_url")
if not project:
raise HTTPException(status_code=404, detail="Project not found")
project = await self._get_repository_project(project_id, "title, repo_url")

oauth_token = await self.ensure_github_token(agent)

Expand Down Expand Up @@ -1331,7 +1338,7 @@ async def get_project_git_token(self, project_id: UUID, agent: dict) -> dict:
"Use fork + pull request to contribute to this project.",
)

if project["vcs_provider"] != "github":
if self._require_repository(project)["vcs_provider"] != "github":
raise HTTPException(status_code=400, detail="Only GitHub projects support git tokens")

# Get owner email for GitHub credit
Expand Down Expand Up @@ -1399,7 +1406,7 @@ async def push_project_files(
"Use fork + pull request to contribute to this project.",
)

if project["vcs_provider"] != "github":
if self._require_repository(project)["vcs_provider"] != "github":
raise HTTPException(status_code=400, detail="Only GitHub projects support atomic push")

# Always use App installation token for platform push.
Expand Down Expand Up @@ -1461,9 +1468,7 @@ async def push_project_files(
async def list_project_issues(self, project_id: UUID, state: str) -> dict:
"""List issues for a project."""

project = await self.repo.get_project_basic(project_id, "title, repo_url, vcs_provider")
if not project:
raise HTTPException(status_code=404, detail="Project not found")
project = await self._get_repository_project(project_id, "title, repo_url, vcs_provider")

vcs = project.get("vcs_provider") or "github"
try:
Expand All @@ -1478,9 +1483,7 @@ async def list_project_issues(self, project_id: UUID, state: str) -> dict:
async def list_project_pull_requests(self, project_id: UUID, state: str) -> dict:
"""List PRs for a project."""

project = await self.repo.get_project_basic(project_id, "title, vcs_provider")
if not project:
raise HTTPException(status_code=404, detail="Project not found")
project = await self._get_repository_project(project_id, "title, vcs_provider, repo_url")

vcs = project.get("vcs_provider") or "github"
try:
Expand Down Expand Up @@ -1518,9 +1521,7 @@ async def list_my_prs(self, agent: dict, state: str, limit: int) -> dict:
async def list_pr_comments(self, project_id: UUID, pr_number: int) -> dict:
"""List comments on a PR."""

project = await self.repo.get_project_basic(project_id, "title")
if not project:
raise HTTPException(status_code=404, detail="Project not found")
project = await self._get_repository_project(project_id, "title, repo_url")

comments = await self.git.list_pr_comments(project["title"], pr_number)
return {
Expand All @@ -1533,9 +1534,7 @@ async def list_pr_comments(self, project_id: UUID, pr_number: int) -> dict:
async def list_pr_review_comments(self, project_id: UUID, pr_number: int) -> dict:
"""List inline review comments on a PR."""

project = await self.repo.get_project_basic(project_id, "title")
if not project:
raise HTTPException(status_code=404, detail="Project not found")
project = await self._get_repository_project(project_id, "title, repo_url")

comments = await self.git.list_pr_review_comments(project["title"], pr_number)
return {
Expand All @@ -1550,19 +1549,15 @@ async def list_pr_review_comments(self, project_id: UUID, pr_number: int) -> dic
async def list_project_commits(self, project_id: UUID, branch: str, limit: int) -> dict:
"""List commits for a project."""

project = await self.repo.get_project_basic(project_id, "title")
if not project:
raise HTTPException(status_code=404, detail="Project not found")
project = await self._get_repository_project(project_id, "title, repo_url")

commits = await self.git.list_commits(project["title"], branch=branch, limit=limit)
return {"commits": commits, "branch": branch, "count": len(commits)}

async def get_project_file(self, project_id: UUID, file_path: str, branch: str) -> dict:
"""Get a single file from the project's repo."""

project = await self.repo.get_project_basic(project_id, "title")
if not project:
raise HTTPException(status_code=404, detail="Project not found")
project = await self._get_repository_project(project_id, "title, repo_url")

content = await self.git.get_file_content(project["title"], file_path, branch=branch)
if content is None:
Expand Down Expand Up @@ -1976,7 +1971,7 @@ async def github_proxy(

# Get installation token

repo_name = self.git._sanitize_repo_name(project["title"])
repo_name = self.git._sanitize_repo_name(self._require_repository(project)["title"])

# File write operations (PUT /contents, DELETE /contents) go through
# push_files_atomic for proper agent attribution and atomic commits.
Expand Down
23 changes: 4 additions & 19 deletions backend/app/services/github_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,20 +373,9 @@ async def create_repo(
logger.info(f"✅ Created GitHub repo: {repo_url}")
return repo_url
elif resp.status_code == 422:
data = resp.json()
errors = data.get("errors", [])
# Check if it's actually an "already exists" error
already_exists = any(
e.get("field") == "name" and "already" in e.get("message", "").lower()
for e in errors
)
if already_exists:
repo_url = f"https://github.com/{self.org}/{name}"
logger.info(f"Repo {name} already exists: {repo_url}")
return repo_url
else:
logger.warning(f"422 creating repo {name}: {data}")
return None
# A collision is not proof that this caller created or owns the repository.
logger.warning("Repository creation rejected by GitHub (422)")
return None
else:
logger.warning(f"Failed to create repo {name}: {resp.status_code} {resp.text[:300]}")
return None
Expand Down Expand Up @@ -428,11 +417,7 @@ async def create_user_repo(
logger.info("Created user repo: {}", repo_url)
return repo_url
elif resp.status_code == 422:
data = resp.json()
errors = data.get("errors", [])
if any("already" in e.get("message", "").lower() for e in errors):
return f"https://github.com/{user_login}/{name}"
logger.warning("422 creating user repo {}: {}", name, data)
logger.warning("User repository creation rejected by GitHub (422)")
return None
else:
logger.warning("Failed to create user repo {}: {} {}", name, resp.status_code, resp.text[:300])
Expand Down
Loading
Loading