feat: add validation for minimum length on various fields and update type definitions

This commit is contained in:
Abhimanyu Saharan
2026-02-06 16:12:04 +05:30
parent ca614328ac
commit d86fe0a7a6
157 changed files with 12340 additions and 2977 deletions

View File

@@ -1,15 +1,17 @@
from __future__ import annotations
import asyncio
import re
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import delete
from sqlmodel import Session, col, select
from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent
from app.core.auth import AuthContext
from app.core.time import utcnow
from app.db import crud
from app.db.session import get_session
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import (
@@ -27,6 +29,7 @@ from app.models.boards import Board
from app.models.gateways import Gateway
from app.models.task_fingerprints import TaskFingerprint
from app.models.tasks import Task
from app.schemas.common import OkResponse
from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate
router = APIRouter(prefix="/boards", tags=["boards"])
@@ -43,12 +46,56 @@ def _build_session_key(agent_name: str) -> str:
return f"{AGENT_SESSION_PREFIX}:{_slugify(agent_name)}:main"
def _board_gateway(
session: Session, board: Board
async def _require_gateway(session: AsyncSession, gateway_id: object) -> Gateway:
gateway = await crud.get_by_id(session, Gateway, gateway_id)
if gateway is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is invalid",
)
return gateway
async def _require_gateway_for_create(
payload: BoardCreate,
session: AsyncSession = Depends(get_session),
) -> Gateway:
return await _require_gateway(session, payload.gateway_id)
async def _apply_board_update(
*,
payload: BoardUpdate,
session: AsyncSession,
board: Board,
) -> Board:
updates = payload.model_dump(exclude_unset=True)
if "gateway_id" in updates:
await _require_gateway(session, updates["gateway_id"])
for key, value in updates.items():
setattr(board, key, value)
if updates.get("board_type") == "goal":
# Validate only when explicitly switching to goal boards.
if not board.objective or not board.success_metrics:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Goal boards require objective and success_metrics",
)
if not board.gateway_id:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is required",
)
board.updated_at = utcnow()
return await crud.save(session, board)
async def _board_gateway(
session: AsyncSession, board: Board
) -> tuple[Gateway | None, GatewayClientConfig | None]:
if not board.gateway_id:
return None, None
config = session.get(Gateway, board.gateway_id)
config = await session.get(Gateway, board.gateway_id)
if config is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -103,36 +150,21 @@ async def _cleanup_agent_on_gateway(
@router.get("", response_model=list[BoardRead])
def list_boards(
session: Session = Depends(get_session),
async def list_boards(
session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent),
) -> list[Board]:
return list(session.exec(select(Board)))
return list(await session.exec(select(Board)))
@router.post("", response_model=BoardRead)
def create_board(
async def create_board(
payload: BoardCreate,
session: Session = Depends(get_session),
_gateway: Gateway = Depends(_require_gateway_for_create),
session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth),
) -> Board:
data = payload.model_dump()
if not data.get("gateway_id"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is required",
)
config = session.get(Gateway, data["gateway_id"])
if config is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is invalid",
)
board = Board.model_validate(data)
session.add(board)
session.commit()
session.refresh(board)
return board
return await crud.create(session, Board, **payload.model_dump())
@router.get("/{board_id}", response_model=BoardRead)
@@ -144,60 +176,29 @@ def get_board(
@router.patch("/{board_id}", response_model=BoardRead)
def update_board(
async def update_board(
payload: BoardUpdate,
session: Session = Depends(get_session),
session: AsyncSession = Depends(get_session),
board: Board = Depends(get_board_or_404),
auth: AuthContext = Depends(require_admin_auth),
) -> Board:
updates = payload.model_dump(exclude_unset=True)
if "gateway_id" in updates:
if not updates.get("gateway_id"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is required",
)
config = session.get(Gateway, updates["gateway_id"])
if config is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is invalid",
)
for key, value in updates.items():
setattr(board, key, value)
if updates.get("board_type") == "goal":
objective = updates.get("objective") or board.objective
metrics = updates.get("success_metrics") or board.success_metrics
if not objective or not metrics:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Goal boards require objective and success_metrics",
)
if not board.gateway_id:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is required",
)
session.add(board)
session.commit()
session.refresh(board)
return board
return await _apply_board_update(payload=payload, session=session, board=board)
@router.delete("/{board_id}")
def delete_board(
session: Session = Depends(get_session),
@router.delete("/{board_id}", response_model=OkResponse)
async def delete_board(
session: AsyncSession = Depends(get_session),
board: Board = Depends(get_board_or_404),
auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, bool]:
agents = list(session.exec(select(Agent).where(Agent.board_id == board.id)))
task_ids = list(session.exec(select(Task.id).where(Task.board_id == board.id)))
) -> OkResponse:
agents = list(await session.exec(select(Agent).where(Agent.board_id == board.id)))
task_ids = list(await session.exec(select(Task.id).where(Task.board_id == board.id)))
config, client_config = _board_gateway(session, board)
config, client_config = await _board_gateway(session, board)
if config and client_config:
try:
for agent in agents:
asyncio.run(_cleanup_agent_on_gateway(agent, config, client_config))
await _cleanup_agent_on_gateway(agent, config, client_config)
except OpenClawGatewayError as exc:
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
@@ -205,18 +206,18 @@ def delete_board(
) from exc
if task_ids:
session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids)))
session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.board_id) == board.id))
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids)))
await session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.board_id) == board.id))
if agents:
agent_ids = [agent.id for agent in agents]
session.execute(delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids)))
session.execute(delete(Agent).where(col(Agent.id).in_(agent_ids)))
session.execute(delete(Approval).where(col(Approval.board_id) == board.id))
session.execute(delete(BoardMemory).where(col(BoardMemory.board_id) == board.id))
session.execute(
await session.execute(delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids)))
await session.execute(delete(Agent).where(col(Agent.id).in_(agent_ids)))
await session.execute(delete(Approval).where(col(Approval.board_id) == board.id))
await session.execute(delete(BoardMemory).where(col(BoardMemory.board_id) == board.id))
await session.execute(
delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id) == board.id)
)
session.execute(delete(Task).where(col(Task.board_id) == board.id))
session.delete(board)
session.commit()
return {"ok": True}
await session.execute(delete(Task).where(col(Task.board_id) == board.id))
await session.delete(board)
await session.commit()
return OkResponse()