Files
openclaw-mission-control/backend/app/api/agents.py
Hugh Brown cc50877131 refactor: rename require_admin_auth/require_admin_or_agent to require_user_auth/require_user_or_agent
These dependencies check actor type (human user vs agent), not admin
privilege. The old names were misleading and could cause authorization
mistakes when wiring new endpoints. Renamed across all 10 consumer
files along with their local ADMIN_AUTH_DEP / ADMIN_OR_AGENT_DEP
aliases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:35:10 +05:30

169 lines
5.1 KiB
Python

"""Thin API wrappers for async agent lifecycle operations."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from uuid import UUID
from fastapi import APIRouter, Depends, Query, Request
from sse_starlette.sse import EventSourceResponse
from app.api.deps import ActorContext, require_user_or_agent, require_org_admin
from app.core.auth import AuthContext, get_auth_context
from app.db.session import get_session
from app.schemas.agents import (
AgentCreate,
AgentHeartbeat,
AgentHeartbeatCreate,
AgentRead,
AgentUpdate,
)
from app.schemas.common import OkResponse
from app.schemas.pagination import DefaultLimitOffsetPage
from app.services.openclaw.provisioning_db import AgentLifecycleService, AgentUpdateOptions
from app.services.organizations import OrganizationContext
if TYPE_CHECKING:
from fastapi_pagination.limit_offset import LimitOffsetPage
from sqlmodel.ext.asyncio.session import AsyncSession
router = APIRouter(prefix="/agents", tags=["agents"])
BOARD_ID_QUERY = Query(default=None)
GATEWAY_ID_QUERY = Query(default=None)
SINCE_QUERY = Query(default=None)
SESSION_DEP = Depends(get_session)
ORG_ADMIN_DEP = Depends(require_org_admin)
ACTOR_DEP = Depends(require_user_or_agent)
AUTH_DEP = Depends(get_auth_context)
@dataclass(frozen=True, slots=True)
class _AgentUpdateParams:
force: bool
auth: AuthContext
ctx: OrganizationContext
def _agent_update_params(
*,
force: bool = False,
auth: AuthContext = AUTH_DEP,
ctx: OrganizationContext = ORG_ADMIN_DEP,
) -> _AgentUpdateParams:
return _AgentUpdateParams(force=force, auth=auth, ctx=ctx)
AGENT_UPDATE_PARAMS_DEP = Depends(_agent_update_params)
@router.get("", response_model=DefaultLimitOffsetPage[AgentRead])
async def list_agents(
board_id: UUID | None = BOARD_ID_QUERY,
gateway_id: UUID | None = GATEWAY_ID_QUERY,
session: AsyncSession = SESSION_DEP,
ctx: OrganizationContext = ORG_ADMIN_DEP,
) -> LimitOffsetPage[AgentRead]:
"""List agents visible to the active organization admin."""
service = AgentLifecycleService(session)
return await service.list_agents(
board_id=board_id,
gateway_id=gateway_id,
ctx=ctx,
)
@router.get("/stream")
async def stream_agents(
request: Request,
board_id: UUID | None = BOARD_ID_QUERY,
since: str | None = SINCE_QUERY,
session: AsyncSession = SESSION_DEP,
ctx: OrganizationContext = ORG_ADMIN_DEP,
) -> EventSourceResponse:
"""Stream agent updates as SSE events."""
service = AgentLifecycleService(session)
return await service.stream_agents(
request=request,
board_id=board_id,
since=since,
ctx=ctx,
)
@router.post("", response_model=AgentRead)
async def create_agent(
payload: AgentCreate,
session: AsyncSession = SESSION_DEP,
actor: ActorContext = ACTOR_DEP,
) -> AgentRead:
"""Create and provision an agent."""
service = AgentLifecycleService(session)
return await service.create_agent(payload=payload, actor=actor)
@router.get("/{agent_id}", response_model=AgentRead)
async def get_agent(
agent_id: str,
session: AsyncSession = SESSION_DEP,
ctx: OrganizationContext = ORG_ADMIN_DEP,
) -> AgentRead:
"""Get a single agent by id."""
service = AgentLifecycleService(session)
return await service.get_agent(agent_id=agent_id, ctx=ctx)
@router.patch("/{agent_id}", response_model=AgentRead)
async def update_agent(
agent_id: str,
payload: AgentUpdate,
params: _AgentUpdateParams = AGENT_UPDATE_PARAMS_DEP,
session: AsyncSession = SESSION_DEP,
) -> AgentRead:
"""Update agent metadata and optionally reprovision."""
service = AgentLifecycleService(session)
return await service.update_agent(
agent_id=agent_id,
payload=payload,
options=AgentUpdateOptions(
force=params.force,
user=params.auth.user,
context=params.ctx,
),
)
@router.post("/{agent_id}/heartbeat", response_model=AgentRead)
async def heartbeat_agent(
agent_id: str,
payload: AgentHeartbeat,
session: AsyncSession = SESSION_DEP,
actor: ActorContext = ACTOR_DEP,
) -> AgentRead:
"""Record a heartbeat for a specific agent."""
service = AgentLifecycleService(session)
return await service.heartbeat_agent(agent_id=agent_id, payload=payload, actor=actor)
@router.post("/heartbeat", response_model=AgentRead)
async def heartbeat_or_create_agent(
payload: AgentHeartbeatCreate,
session: AsyncSession = SESSION_DEP,
actor: ActorContext = ACTOR_DEP,
) -> AgentRead:
"""Heartbeat an existing agent or create/provision one if needed."""
service = AgentLifecycleService(session)
return await service.heartbeat_or_create_agent(payload=payload, actor=actor)
@router.delete("/{agent_id}", response_model=OkResponse)
async def delete_agent(
agent_id: str,
session: AsyncSession = SESSION_DEP,
ctx: OrganizationContext = ORG_ADMIN_DEP,
) -> OkResponse:
"""Delete an agent and clean related task state."""
service = AgentLifecycleService(session)
return await service.delete_agent(agent_id=agent_id, ctx=ctx)