"""FastAPI router for delegation lifecycle. Endpoints (originally from llm-principal-broker, now in-process): POST /delegations/ create_delegation §8.1 POST /delegations/{id}/revoke revoke_delegation §8.2 GET /delegations/{id} get_delegation §8.3 GET /delegations/ list_delegations §8.4 """ from datetime import datetime from fastapi import APIRouter, Header, HTTPException from .lifecycle import DelegationManager from .models import ( ActiveDelegation, AgentListResponse, DelegationInfo, DelegationRequest, DelegationResponse, DelegationStatus, RevokeRequest, RevokeResponse, ) from .storage import get_active_delegations, get_delegation as db_get router = APIRouter(prefix="/delegations", tags=["Delegations"]) # A single DelegationManager instance is shared across requests. It holds the # AgentRegistrar (Keycloak/Entra/Stub) and is constructed once at import time. manager = DelegationManager() @router.post("/", response_model=DelegationResponse) async def create_delegation( request: DelegationRequest, x_delegator_did: str = Header(..., alias="X-Delegator-DID"), ): """Request delegation of authority to an AI agent (§8.1).""" try: return await manager.create_delegation(request, x_delegator_did) except RuntimeError as e: raise HTTPException(status_code=502, detail=str(e)) @router.post("/{delegation_id}/revoke", response_model=RevokeResponse) async def revoke_delegation( delegation_id: str, request: RevokeRequest = RevokeRequest(), ): """Revoke an active delegation (§8.2).""" success = await manager.revoke_delegation(delegation_id, request.reason) if not success: raise HTTPException(status_code=404, detail="Delegation not found or not active") return RevokeResponse(delegation_id=delegation_id, reason=request.reason) @router.get("/{delegation_id}", response_model=DelegationInfo) async def get_delegation(delegation_id: str): """Query delegation status (§8.3).""" d = await db_get(delegation_id) if not d: raise HTTPException(status_code=404, detail="Delegation not found") now = datetime.utcnow() ttl_remaining = max(0, int((d.expires_at - now).total_seconds())) return DelegationInfo( delegation_id=d.delegation_id, status=DelegationStatus(d.status), agent_did=d.agent_did, agent_type=d.agent_type, delegator_did=d.delegator_did, commands_executed=d.commands_executed, commands_remaining=max(0, d.max_commands - d.commands_executed), ttl_remaining_seconds=ttl_remaining, created_at=d.created_at.isoformat(), expires_at=d.expires_at.isoformat(), ) @router.get("/", response_model=AgentListResponse) async def list_delegations(): """List all active agent delegations (§8.4).""" active = await get_active_delegations() now = datetime.utcnow() return AgentListResponse( active_delegations=[ ActiveDelegation( delegation_id=d.delegation_id, agent_type=d.agent_type, delegator=d.delegator_did, commands_executed=d.commands_executed, ttl_remaining_seconds=max( 0, int((d.expires_at - now).total_seconds()) ), status=d.status, ) for d in active ], total_active=len(active), )