- 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>
489 lines
16 KiB
Python
489 lines
16 KiB
Python
"""Add Syndarix models
|
|
|
|
Revision ID: 0004
|
|
Revises: 0003
|
|
Create Date: 2025-12-30
|
|
|
|
This migration creates the core Syndarix domain tables:
|
|
- projects: Client engagement projects
|
|
- agent_types: Agent template configurations
|
|
- agent_instances: Spawned agent instances assigned to projects
|
|
- issues: Work items (stories, tasks, bugs)
|
|
- sprints: Sprint containers for issues
|
|
"""
|
|
|
|
from collections.abc import Sequence
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = "0004"
|
|
down_revision: str | None = "0003"
|
|
branch_labels: str | Sequence[str] | None = None
|
|
depends_on: str | Sequence[str] | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
"""Create Syndarix domain tables."""
|
|
|
|
# Create ENUM types first
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE autonomy_level AS ENUM (
|
|
'full_control', 'milestone', 'autonomous'
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE project_status AS ENUM (
|
|
'active', 'paused', 'completed', 'archived'
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE project_complexity AS ENUM (
|
|
'script', 'simple', 'medium', 'complex'
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE client_mode AS ENUM (
|
|
'technical', 'auto'
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE agent_status AS ENUM (
|
|
'idle', 'working', 'waiting', 'paused', 'terminated'
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE issue_status AS ENUM (
|
|
'open', 'in_progress', 'in_review', 'closed', 'blocked'
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE issue_priority AS ENUM (
|
|
'critical', 'high', 'medium', 'low'
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE external_tracker_type AS ENUM (
|
|
'gitea', 'github', 'gitlab', 'jira'
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE sync_status AS ENUM (
|
|
'synced', 'pending', 'conflict', 'error'
|
|
)
|
|
"""
|
|
)
|
|
op.execute(
|
|
"""
|
|
CREATE TYPE sprint_status AS ENUM (
|
|
'planned', 'active', 'completed', 'cancelled'
|
|
)
|
|
"""
|
|
)
|
|
|
|
# Create projects table
|
|
op.create_table(
|
|
"projects",
|
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("name", sa.String(255), nullable=False),
|
|
sa.Column("slug", sa.String(255), nullable=False),
|
|
sa.Column("description", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"autonomy_level",
|
|
sa.Enum(
|
|
"full_control",
|
|
"milestone",
|
|
"autonomous",
|
|
name="autonomy_level",
|
|
create_type=False,
|
|
),
|
|
nullable=False,
|
|
server_default="milestone",
|
|
),
|
|
sa.Column(
|
|
"status",
|
|
sa.Enum(
|
|
"active",
|
|
"paused",
|
|
"completed",
|
|
"archived",
|
|
name="project_status",
|
|
create_type=False,
|
|
),
|
|
nullable=False,
|
|
server_default="active",
|
|
),
|
|
sa.Column(
|
|
"complexity",
|
|
sa.Enum(
|
|
"script",
|
|
"simple",
|
|
"medium",
|
|
"complex",
|
|
name="project_complexity",
|
|
create_type=False,
|
|
),
|
|
nullable=False,
|
|
server_default="medium",
|
|
),
|
|
sa.Column(
|
|
"client_mode",
|
|
sa.Enum("technical", "auto", name="client_mode", create_type=False),
|
|
nullable=False,
|
|
server_default="auto",
|
|
),
|
|
sa.Column(
|
|
"settings", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default="{}"
|
|
),
|
|
sa.Column("owner_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.ForeignKeyConstraint(
|
|
["owner_id"], ["users.id"], ondelete="SET NULL"
|
|
),
|
|
sa.UniqueConstraint("slug"),
|
|
)
|
|
op.create_index("ix_projects_name", "projects", ["name"])
|
|
op.create_index("ix_projects_slug", "projects", ["slug"])
|
|
op.create_index("ix_projects_status", "projects", ["status"])
|
|
op.create_index("ix_projects_autonomy_level", "projects", ["autonomy_level"])
|
|
op.create_index("ix_projects_complexity", "projects", ["complexity"])
|
|
op.create_index("ix_projects_client_mode", "projects", ["client_mode"])
|
|
op.create_index("ix_projects_owner_id", "projects", ["owner_id"])
|
|
op.create_index("ix_projects_slug_status", "projects", ["slug", "status"])
|
|
op.create_index("ix_projects_owner_status", "projects", ["owner_id", "status"])
|
|
op.create_index(
|
|
"ix_projects_autonomy_status", "projects", ["autonomy_level", "status"]
|
|
)
|
|
op.create_index(
|
|
"ix_projects_complexity_status", "projects", ["complexity", "status"]
|
|
)
|
|
|
|
# Create agent_types table
|
|
op.create_table(
|
|
"agent_types",
|
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("name", sa.String(100), nullable=False),
|
|
sa.Column("slug", sa.String(100), nullable=False),
|
|
sa.Column("description", sa.Text(), nullable=True),
|
|
sa.Column("primary_model", sa.String(100), nullable=False),
|
|
sa.Column(
|
|
"fallback_models",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
nullable=False,
|
|
server_default="[]",
|
|
),
|
|
sa.Column("system_prompt", sa.Text(), nullable=True),
|
|
sa.Column("personality_prompt", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"capabilities",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
nullable=False,
|
|
server_default="[]",
|
|
),
|
|
sa.Column(
|
|
"default_config",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
nullable=False,
|
|
server_default="{}",
|
|
),
|
|
sa.Column("is_active", sa.Boolean(), nullable=False, server_default="true"),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.UniqueConstraint("slug"),
|
|
)
|
|
op.create_index("ix_agent_types_name", "agent_types", ["name"])
|
|
op.create_index("ix_agent_types_slug", "agent_types", ["slug"])
|
|
op.create_index("ix_agent_types_is_active", "agent_types", ["is_active"])
|
|
op.create_index("ix_agent_types_primary_model", "agent_types", ["primary_model"])
|
|
|
|
# Create agent_instances table
|
|
op.create_table(
|
|
"agent_instances",
|
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("agent_type_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("name", sa.String(100), nullable=False),
|
|
sa.Column(
|
|
"status",
|
|
sa.Enum(
|
|
"idle",
|
|
"working",
|
|
"waiting",
|
|
"paused",
|
|
"terminated",
|
|
name="agent_status",
|
|
create_type=False,
|
|
),
|
|
nullable=False,
|
|
server_default="idle",
|
|
),
|
|
sa.Column("current_task", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"config_overrides",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
nullable=False,
|
|
server_default="{}",
|
|
),
|
|
sa.Column(
|
|
"metadata",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
nullable=False,
|
|
server_default="{}",
|
|
),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.ForeignKeyConstraint(
|
|
["agent_type_id"], ["agent_types.id"], ondelete="RESTRICT"
|
|
),
|
|
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
|
)
|
|
op.create_index("ix_agent_instances_name", "agent_instances", ["name"])
|
|
op.create_index("ix_agent_instances_status", "agent_instances", ["status"])
|
|
op.create_index(
|
|
"ix_agent_instances_agent_type_id", "agent_instances", ["agent_type_id"]
|
|
)
|
|
op.create_index("ix_agent_instances_project_id", "agent_instances", ["project_id"])
|
|
op.create_index(
|
|
"ix_agent_instances_project_status",
|
|
"agent_instances",
|
|
["project_id", "status"],
|
|
)
|
|
op.create_index(
|
|
"ix_agent_instances_type_status",
|
|
"agent_instances",
|
|
["agent_type_id", "status"],
|
|
)
|
|
|
|
# Create sprints table (before issues for FK reference)
|
|
op.create_table(
|
|
"sprints",
|
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("name", sa.String(100), nullable=False),
|
|
sa.Column("number", sa.Integer(), nullable=False),
|
|
sa.Column("goal", sa.Text(), nullable=True),
|
|
sa.Column("start_date", sa.Date(), nullable=True),
|
|
sa.Column("end_date", sa.Date(), nullable=True),
|
|
sa.Column(
|
|
"status",
|
|
sa.Enum(
|
|
"planned",
|
|
"active",
|
|
"completed",
|
|
"cancelled",
|
|
name="sprint_status",
|
|
create_type=False,
|
|
),
|
|
nullable=False,
|
|
server_default="planned",
|
|
),
|
|
sa.Column("planned_points", sa.Integer(), nullable=True),
|
|
sa.Column("velocity", sa.Integer(), nullable=True),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
|
sa.UniqueConstraint("project_id", "number", name="uq_sprint_project_number"),
|
|
)
|
|
op.create_index("ix_sprints_name", "sprints", ["name"])
|
|
op.create_index("ix_sprints_number", "sprints", ["number"])
|
|
op.create_index("ix_sprints_status", "sprints", ["status"])
|
|
op.create_index("ix_sprints_project_id", "sprints", ["project_id"])
|
|
op.create_index("ix_sprints_project_status", "sprints", ["project_id", "status"])
|
|
|
|
# Create issues table
|
|
op.create_table(
|
|
"issues",
|
|
sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("project_id", postgresql.UUID(as_uuid=True), nullable=False),
|
|
sa.Column("sprint_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
sa.Column("assigned_agent_id", postgresql.UUID(as_uuid=True), nullable=True),
|
|
sa.Column("title", sa.String(500), nullable=False),
|
|
sa.Column("description", sa.Text(), nullable=True),
|
|
sa.Column(
|
|
"status",
|
|
sa.Enum(
|
|
"open",
|
|
"in_progress",
|
|
"in_review",
|
|
"closed",
|
|
"blocked",
|
|
name="issue_status",
|
|
create_type=False,
|
|
),
|
|
nullable=False,
|
|
server_default="open",
|
|
),
|
|
sa.Column(
|
|
"priority",
|
|
sa.Enum(
|
|
"critical", "high", "medium", "low", name="issue_priority", create_type=False
|
|
),
|
|
nullable=False,
|
|
server_default="medium",
|
|
),
|
|
sa.Column("story_points", sa.Integer(), nullable=True),
|
|
sa.Column(
|
|
"labels",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
nullable=False,
|
|
server_default="[]",
|
|
),
|
|
sa.Column(
|
|
"external_tracker",
|
|
sa.Enum(
|
|
"gitea",
|
|
"github",
|
|
"gitlab",
|
|
"jira",
|
|
name="external_tracker_type",
|
|
create_type=False,
|
|
),
|
|
nullable=True,
|
|
),
|
|
sa.Column("external_id", sa.String(255), nullable=True),
|
|
sa.Column("external_url", sa.String(2048), nullable=True),
|
|
sa.Column("external_number", sa.Integer(), nullable=True),
|
|
sa.Column(
|
|
"sync_status",
|
|
sa.Enum(
|
|
"synced",
|
|
"pending",
|
|
"conflict",
|
|
"error",
|
|
name="sync_status",
|
|
create_type=False,
|
|
),
|
|
nullable=True,
|
|
),
|
|
sa.Column("last_synced_at", sa.DateTime(timezone=True), nullable=True),
|
|
sa.Column(
|
|
"metadata",
|
|
postgresql.JSONB(astext_type=sa.Text()),
|
|
nullable=False,
|
|
server_default="{}",
|
|
),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.Column(
|
|
"updated_at",
|
|
sa.DateTime(timezone=True),
|
|
nullable=False,
|
|
server_default=sa.text("now()"),
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
sa.ForeignKeyConstraint(["project_id"], ["projects.id"], ondelete="CASCADE"),
|
|
sa.ForeignKeyConstraint(["sprint_id"], ["sprints.id"], ondelete="SET NULL"),
|
|
sa.ForeignKeyConstraint(
|
|
["assigned_agent_id"], ["agent_instances.id"], ondelete="SET NULL"
|
|
),
|
|
)
|
|
op.create_index("ix_issues_title", "issues", ["title"])
|
|
op.create_index("ix_issues_status", "issues", ["status"])
|
|
op.create_index("ix_issues_priority", "issues", ["priority"])
|
|
op.create_index("ix_issues_project_id", "issues", ["project_id"])
|
|
op.create_index("ix_issues_sprint_id", "issues", ["sprint_id"])
|
|
op.create_index("ix_issues_assigned_agent_id", "issues", ["assigned_agent_id"])
|
|
op.create_index("ix_issues_external_tracker", "issues", ["external_tracker"])
|
|
op.create_index("ix_issues_sync_status", "issues", ["sync_status"])
|
|
op.create_index("ix_issues_project_status", "issues", ["project_id", "status"])
|
|
op.create_index(
|
|
"ix_issues_project_status_priority",
|
|
"issues",
|
|
["project_id", "status", "priority"],
|
|
)
|
|
op.create_index(
|
|
"ix_issues_external",
|
|
"issues",
|
|
["project_id", "external_tracker", "external_id"],
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
"""Drop Syndarix domain tables."""
|
|
# Drop tables in reverse order (respecting FK constraints)
|
|
op.drop_table("issues")
|
|
op.drop_table("sprints")
|
|
op.drop_table("agent_instances")
|
|
op.drop_table("agent_types")
|
|
op.drop_table("projects")
|
|
|
|
# Drop ENUM types
|
|
op.execute("DROP TYPE IF EXISTS sprint_status")
|
|
op.execute("DROP TYPE IF EXISTS sync_status")
|
|
op.execute("DROP TYPE IF EXISTS external_tracker_type")
|
|
op.execute("DROP TYPE IF EXISTS issue_priority")
|
|
op.execute("DROP TYPE IF EXISTS issue_status")
|
|
op.execute("DROP TYPE IF EXISTS agent_status")
|
|
op.execute("DROP TYPE IF EXISTS client_mode")
|
|
op.execute("DROP TYPE IF EXISTS project_complexity")
|
|
op.execute("DROP TYPE IF EXISTS project_status")
|
|
op.execute("DROP TYPE IF EXISTS autonomy_level")
|