fix: Add missing API endpoints and validation improvements

- Add cancel_sprint and delete_sprint endpoints to sprints.py
- Add unassign_issue endpoint to issues.py
- Add remove_issue_from_sprint endpoint to sprints.py
- Add CRUD methods: remove_sprint_from_issues, unassign, remove_from_sprint
- Add validation to prevent closed issues in active/planned sprints
- Add authorization tests for SSE events endpoint
- Fix IDOR vulnerabilities in agents.py and projects.py
- Add Syndarix models migration (0004)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 15:39:51 +01:00
parent b43fa8ace2
commit cea97afe25
10 changed files with 1156 additions and 74 deletions

View File

@@ -7,6 +7,7 @@ within their projects, including spawning, pausing, resuming, and terminating ag
"""
import logging
import os
from typing import Any
from uuid import UUID
@@ -23,6 +24,7 @@ from app.core.exceptions import (
ValidationException,
)
from app.crud.syndarix.agent_instance import agent_instance as agent_instance_crud
from app.crud.syndarix.agent_type import agent_type as agent_type_crud
from app.crud.syndarix.project import project as project_crud
from app.models.syndarix import AgentInstance, Project
from app.models.syndarix.enums import AgentStatus
@@ -47,6 +49,10 @@ logger = logging.getLogger(__name__)
# Initialize limiter for this router
limiter = Limiter(key_func=get_remote_address)
# Use higher rate limits in test environment
IS_TEST = os.getenv("IS_TEST", "False") == "True"
RATE_MULTIPLIER = 100 if IS_TEST else 1
# Valid status transitions for agent lifecycle management
VALID_STATUS_TRANSITIONS: dict[AgentStatus, set[AgentStatus]] = {
@@ -173,7 +179,7 @@ def build_agent_response(
description="Spawn a new agent instance in a project. Requires project ownership or superuser.",
operation_id="spawn_agent",
)
@limiter.limit("20/minute")
@limiter.limit(f"{20 * RATE_MULTIPLIER}/minute")
async def spawn_agent(
request: Request,
project_id: UUID,
@@ -214,6 +220,20 @@ async def spawn_agent(
field="project_id",
)
# Validate that the agent type exists and is active
agent_type = await agent_type_crud.get(db, id=agent_in.agent_type_id)
if not agent_type:
raise NotFoundError(
message=f"Agent type {agent_in.agent_type_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
if not agent_type.is_active:
raise ValidationException(
message=f"Agent type '{agent_type.name}' is inactive and cannot be used",
error_code=ErrorCode.VALIDATION_ERROR,
field="agent_type_id",
)
# Create the agent instance
agent = await agent_instance_crud.create(db, obj_in=agent_in)
@@ -256,7 +276,7 @@ async def spawn_agent(
description="List all agent instances in a project with optional filtering.",
operation_id="list_project_agents",
)
@limiter.limit("60/minute")
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
async def list_project_agents(
request: Request,
project_id: UUID,
@@ -350,7 +370,7 @@ async def list_project_agents(
description="Get detailed information about a specific agent instance.",
operation_id="get_agent",
)
@limiter.limit("60/minute")
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
async def get_agent(
request: Request,
project_id: UUID,
@@ -427,7 +447,7 @@ async def get_agent(
description="Update an agent instance's configuration and state.",
operation_id="update_agent",
)
@limiter.limit("30/minute")
@limiter.limit(f"{30 * RATE_MULTIPLIER}/minute")
async def update_agent(
request: Request,
project_id: UUID,
@@ -522,7 +542,7 @@ async def update_agent(
description="Pause an agent instance, temporarily stopping its work.",
operation_id="pause_agent",
)
@limiter.limit("20/minute")
@limiter.limit(f"{20 * RATE_MULTIPLIER}/minute")
async def pause_agent(
request: Request,
project_id: UUID,
@@ -621,7 +641,7 @@ async def pause_agent(
description="Resume a paused agent instance.",
operation_id="resume_agent",
)
@limiter.limit("20/minute")
@limiter.limit(f"{20 * RATE_MULTIPLIER}/minute")
async def resume_agent(
request: Request,
project_id: UUID,
@@ -720,7 +740,7 @@ async def resume_agent(
description="Terminate an agent instance, permanently stopping it.",
operation_id="terminate_agent",
)
@limiter.limit("10/minute")
@limiter.limit(f"{10 * RATE_MULTIPLIER}/minute")
async def terminate_agent(
request: Request,
project_id: UUID,
@@ -817,7 +837,7 @@ async def terminate_agent(
description="Get usage metrics for a specific agent instance.",
operation_id="get_agent_metrics",
)
@limiter.limit("60/minute")
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
async def get_agent_metrics(
request: Request,
project_id: UUID,
@@ -897,7 +917,7 @@ async def get_agent_metrics(
description="Get aggregated usage metrics for all agents in a project.",
operation_id="get_project_agent_metrics",
)
@limiter.limit("60/minute")
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
async def get_project_agent_metrics(
request: Request,
project_id: UUID,