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,10 +1,10 @@
from __future__ import annotations
import asyncio
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.api import agents as agents_api
from app.api import approvals as approvals_api
@@ -16,15 +16,20 @@ from app.core.agent_auth import AgentAuthContext, get_agent_auth_context
from app.db.session import get_session
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
from app.models.activity_events import ActivityEvent
from app.models.agents import Agent
from app.models.approvals import Approval
from app.models.board_memory import BoardMemory
from app.models.board_onboarding import BoardOnboardingSession
from app.models.boards import Board
from app.models.gateways import Gateway
from app.models.tasks import Task
from app.schemas.agents import AgentCreate, AgentHeartbeat, AgentHeartbeatCreate, AgentNudge, AgentRead
from app.schemas.approvals import ApprovalCreate, ApprovalRead
from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalStatus
from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead
from app.schemas.board_onboarding import BoardOnboardingRead
from app.schemas.board_onboarding import BoardOnboardingAgentUpdate, BoardOnboardingRead
from app.schemas.boards import BoardRead
from app.schemas.common import OkResponse
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
from app.services.activity_log import record_activity
@@ -40,24 +45,24 @@ def _guard_board_access(agent_ctx: AgentAuthContext, board: Board) -> None:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
def _gateway_config(session: Session, board: Board) -> GatewayClientConfig:
async def _gateway_config(session: AsyncSession, board: Board) -> GatewayClientConfig:
if not board.gateway_id:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
gateway = session.get(Gateway, board.gateway_id)
gateway = await session.get(Gateway, board.gateway_id)
if gateway is None or not gateway.url:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
return GatewayClientConfig(url=gateway.url, token=gateway.token)
@router.get("/boards", response_model=list[BoardRead])
def list_boards(
session: Session = Depends(get_session),
async def list_boards(
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[Board]:
if agent_ctx.agent.board_id:
board = session.get(Board, agent_ctx.agent.board_id)
board = await session.get(Board, agent_ctx.agent.board_id)
return [board] if board else []
return list(session.exec(select(Board)))
return list(await session.exec(select(Board)))
@router.get("/boards/{board_id}", response_model=BoardRead)
@@ -70,10 +75,10 @@ def get_board(
@router.get("/agents", response_model=list[AgentRead])
def list_agents(
async def list_agents(
board_id: UUID | None = Query(default=None),
limit: int | None = Query(default=None, ge=1, le=200),
session: Session = Depends(get_session),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[AgentRead]:
statement = select(Agent)
@@ -85,8 +90,8 @@ def list_agents(
statement = statement.where(Agent.board_id == board_id)
if limit is not None:
statement = statement.limit(limit)
agents = list(session.exec(statement))
main_session_keys = agents_api._get_gateway_main_session_keys(session)
agents = list(await session.exec(statement))
main_session_keys = await agents_api._get_gateway_main_session_keys(session)
return [
agents_api._to_agent_read(agents_api._with_computed_status(agent), main_session_keys)
for agent in agents
@@ -94,17 +99,17 @@ def list_agents(
@router.get("/boards/{board_id}/tasks", response_model=list[TaskRead])
def list_tasks(
async def list_tasks(
status_filter: str | None = Query(default=None, alias="status"),
assigned_agent_id: UUID | None = None,
unassigned: bool | None = None,
limit: int | None = Query(default=None, ge=1, le=200),
board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[TaskRead]:
) -> list[Task]:
_guard_board_access(agent_ctx, board)
return tasks_api.list_tasks(
return await tasks_api.list_tasks(
status_filter=status_filter,
assigned_agent_id=assigned_agent_id,
unassigned=unassigned,
@@ -116,22 +121,21 @@ def list_tasks(
@router.post("/boards/{board_id}/tasks", response_model=TaskRead)
def create_task(
async def create_task(
payload: TaskCreate,
board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> TaskRead:
) -> Task:
_guard_board_access(agent_ctx, board)
if not agent_ctx.agent.is_board_lead:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
tasks_api.validate_task_status(payload.status)
task = Task.model_validate(payload)
task.board_id = board.id
task.auto_created = True
task.auto_reason = f"lead_agent:{agent_ctx.agent.id}"
if task.assigned_agent_id:
agent = session.get(Agent, task.assigned_agent_id)
agent = await session.get(Agent, task.assigned_agent_id)
if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
if agent.is_board_lead:
@@ -142,8 +146,8 @@ def create_task(
if agent.board_id and agent.board_id != board.id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT)
session.add(task)
session.commit()
session.refresh(task)
await session.commit()
await session.refresh(task)
record_activity(
session,
event_type="task.created",
@@ -151,11 +155,11 @@ def create_task(
message=f"Task created by lead: {task.title}.",
agent_id=agent_ctx.agent.id,
)
session.commit()
await session.commit()
if task.assigned_agent_id:
assigned_agent = session.get(Agent, task.assigned_agent_id)
assigned_agent = await session.get(Agent, task.assigned_agent_id)
if assigned_agent:
tasks_api._notify_agent_on_task_assign(
await tasks_api._notify_agent_on_task_assign(
session=session,
board=board,
task=task,
@@ -165,15 +169,15 @@ def create_task(
@router.patch("/boards/{board_id}/tasks/{task_id}", response_model=TaskRead)
def update_task(
async def update_task(
payload: TaskUpdate,
task=Depends(get_task_or_404),
session: Session = Depends(get_session),
task: Task = Depends(get_task_or_404),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> TaskRead:
) -> Task:
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return tasks_api.update_task(
return await tasks_api.update_task(
payload=payload,
task=task,
session=session,
@@ -182,14 +186,14 @@ def update_task(
@router.get("/boards/{board_id}/tasks/{task_id}/comments", response_model=list[TaskCommentRead])
def list_task_comments(
task=Depends(get_task_or_404),
session: Session = Depends(get_session),
async def list_task_comments(
task: Task = Depends(get_task_or_404),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[TaskCommentRead]:
) -> list[ActivityEvent]:
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return tasks_api.list_task_comments(
return await tasks_api.list_task_comments(
task=task,
session=session,
actor=_actor(agent_ctx),
@@ -197,15 +201,15 @@ def list_task_comments(
@router.post("/boards/{board_id}/tasks/{task_id}/comments", response_model=TaskCommentRead)
def create_task_comment(
async def create_task_comment(
payload: TaskCommentCreate,
task=Depends(get_task_or_404),
session: Session = Depends(get_session),
task: Task = Depends(get_task_or_404),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> TaskCommentRead:
) -> ActivityEvent:
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return tasks_api.create_task_comment(
return await tasks_api.create_task_comment(
payload=payload,
task=task,
session=session,
@@ -214,15 +218,15 @@ def create_task_comment(
@router.get("/boards/{board_id}/memory", response_model=list[BoardMemoryRead])
def list_board_memory(
async def list_board_memory(
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
board=Depends(get_board_or_404),
session: Session = Depends(get_session),
board: Board = Depends(get_board_or_404),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[BoardMemoryRead]:
) -> list[BoardMemory]:
_guard_board_access(agent_ctx, board)
return board_memory_api.list_board_memory(
return await board_memory_api.list_board_memory(
limit=limit,
offset=offset,
board=board,
@@ -232,14 +236,14 @@ def list_board_memory(
@router.post("/boards/{board_id}/memory", response_model=BoardMemoryRead)
def create_board_memory(
async def create_board_memory(
payload: BoardMemoryCreate,
board=Depends(get_board_or_404),
session: Session = Depends(get_session),
board: Board = Depends(get_board_or_404),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> BoardMemoryRead:
) -> BoardMemory:
_guard_board_access(agent_ctx, board)
return board_memory_api.create_board_memory(
return await board_memory_api.create_board_memory(
payload=payload,
board=board,
session=session,
@@ -248,14 +252,14 @@ def create_board_memory(
@router.get("/boards/{board_id}/approvals", response_model=list[ApprovalRead])
def list_approvals(
status_filter: str | None = Query(default=None, alias="status"),
board=Depends(get_board_or_404),
session: Session = Depends(get_session),
async def list_approvals(
status_filter: ApprovalStatus | None = Query(default=None, alias="status"),
board: Board = Depends(get_board_or_404),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[ApprovalRead]:
) -> list[Approval]:
_guard_board_access(agent_ctx, board)
return approvals_api.list_approvals(
return await approvals_api.list_approvals(
status_filter=status_filter,
board=board,
session=session,
@@ -264,14 +268,14 @@ def list_approvals(
@router.post("/boards/{board_id}/approvals", response_model=ApprovalRead)
def create_approval(
async def create_approval(
payload: ApprovalCreate,
board=Depends(get_board_or_404),
session: Session = Depends(get_session),
board: Board = Depends(get_board_or_404),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> ApprovalRead:
) -> Approval:
_guard_board_access(agent_ctx, board)
return approvals_api.create_approval(
return await approvals_api.create_approval(
payload=payload,
board=board,
session=session,
@@ -280,14 +284,14 @@ def create_approval(
@router.post("/boards/{board_id}/onboarding", response_model=BoardOnboardingRead)
def update_onboarding(
payload: dict[str, object],
async def update_onboarding(
payload: BoardOnboardingAgentUpdate,
board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> BoardOnboardingRead:
) -> BoardOnboardingSession:
_guard_board_access(agent_ctx, board)
return onboarding_api.agent_onboarding_update(
return await onboarding_api.agent_onboarding_update(
payload=payload,
board=board,
session=session,
@@ -298,7 +302,7 @@ def update_onboarding(
@router.post("/agents", response_model=AgentRead)
async def create_agent(
payload: AgentCreate,
session: Session = Depends(get_session),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> AgentRead:
if not agent_ctx.agent.is_board_lead:
@@ -313,18 +317,18 @@ async def create_agent(
)
@router.post("/boards/{board_id}/agents/{agent_id}/nudge")
def nudge_agent(
@router.post("/boards/{board_id}/agents/{agent_id}/nudge", response_model=OkResponse)
async def nudge_agent(
payload: AgentNudge,
agent_id: str,
board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> dict[str, bool]:
) -> OkResponse:
_guard_board_access(agent_ctx, board)
if not agent_ctx.agent.is_board_lead:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
target = session.get(Agent, agent_id)
target = await session.get(Agent, agent_id)
if target is None or (target.board_id and target.board_id != board.id):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
if not target.openclaw_session_id:
@@ -332,15 +336,9 @@ def nudge_agent(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Target agent has no session key",
)
message = payload.message.strip()
if not message:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="message is required",
)
config = _gateway_config(session, board)
async def _send() -> None:
message = payload.message
config = await _gateway_config(session, board)
try:
await ensure_session(target.openclaw_session_id, config=config, label=target.name)
await send_message(
message,
@@ -348,9 +346,6 @@ def nudge_agent(
config=config,
deliver=True,
)
try:
asyncio.run(_send())
except OpenClawGatewayError as exc:
record_activity(
session,
@@ -358,7 +353,7 @@ def nudge_agent(
message=f"Nudge failed for {target.name}: {exc}",
agent_id=agent_ctx.agent.id,
)
session.commit()
await session.commit()
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
record_activity(
session,
@@ -366,18 +361,18 @@ def nudge_agent(
message=f"Nudge sent to {target.name}.",
agent_id=agent_ctx.agent.id,
)
session.commit()
return {"ok": True}
await session.commit()
return OkResponse()
@router.post("/heartbeat", response_model=AgentRead)
async def agent_heartbeat(
payload: AgentHeartbeatCreate,
session: Session = Depends(get_session),
session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> AgentRead:
# Heartbeats must apply to the authenticated agent; agent names are not unique.
return agents_api.heartbeat_agent( # type: ignore[attr-defined]
return await agents_api.heartbeat_agent(
agent_id=str(agent_ctx.agent.id),
payload=AgentHeartbeat(status=payload.status),
session=session,