Files
microdao-daarion/services/agents-service/routes_agents.py
Apple 3de3c8cb36 feat: Add presence heartbeat for Matrix online status
- matrix-gateway: POST /internal/matrix/presence/online endpoint
- usePresenceHeartbeat hook with activity tracking
- Auto away after 5 min inactivity
- Offline on page close/visibility change
- Integrated in MatrixChatRoom component
2025-11-27 00:19:40 -08:00

265 lines
7.8 KiB
Python

"""
Agent CRUD Routes
Phase 6: Create, Read, Update, Delete agents
"""
from fastapi import APIRouter, HTTPException, Depends, Header
from typing import List, Optional
import httpx
from models import AgentCreate, AgentUpdate, AgentRead, AgentBlueprint
from repository_agents import AgentRepository
from repository_events import EventRepository
router = APIRouter(prefix="/agents", tags=["agents"])
# Dependency injection (will be set in main.py)
agent_repo: Optional[AgentRepository] = None
event_repo: Optional[EventRepository] = None
# Service URLs (from env)
import os
AUTH_SERVICE_URL = os.getenv("AUTH_SERVICE_URL", "http://localhost:7011")
PDP_SERVICE_URL = os.getenv("PDP_SERVICE_URL", "http://localhost:7012")
# ============================================================================
# Auth & PDP Helpers
# ============================================================================
async def get_actor_from_token(authorization: Optional[str] = Header(None)):
"""
Get ActorIdentity from auth-service
Returns actor dict or raises 401
"""
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Missing or invalid authorization header")
token = authorization.replace("Bearer ", "")
async with httpx.AsyncClient() as client:
try:
response = await client.get(
f"{AUTH_SERVICE_URL}/auth/me",
headers={"Authorization": f"Bearer {token}"}
)
response.raise_for_status()
return response.json()
except httpx.HTTPError:
raise HTTPException(status_code=401, detail="Invalid or expired token")
async def check_pdp_permission(
action: str,
resource: dict,
context: dict,
actor: dict
) -> bool:
"""
Check permission via pdp-service
Returns True if allowed, False otherwise
"""
async with httpx.AsyncClient() as client:
try:
response = await client.post(
f"{PDP_SERVICE_URL}/internal/pdp/evaluate",
json={
"action": action,
"resource": resource,
"context": context,
"actor": actor
}
)
response.raise_for_status()
result = response.json()
return result.get("decision") == "ALLOW"
except httpx.HTTPError as e:
print(f"⚠️ PDP error: {e}")
return False # Fail closed
# ============================================================================
# Blueprints
# ============================================================================
@router.get("/blueprints", response_model=List[AgentBlueprint])
async def list_blueprints():
"""List all agent blueprints"""
return await agent_repo.list_blueprints()
# ============================================================================
# CRUD — Create
# ============================================================================
@router.post("", response_model=AgentRead, status_code=201)
async def create_agent(
data: AgentCreate,
actor: dict = Depends(get_actor_from_token)
):
"""
Create new agent
Requires: MANAGE permission on AGENT:*
"""
# Check PDP
allowed = await check_pdp_permission(
action="MANAGE",
resource={"type": "AGENT", "id": "*"},
context={"operation": "create", "kind": data.kind.value},
actor=actor
)
if not allowed:
raise HTTPException(status_code=403, detail="Permission denied: cannot create agent")
# Create agent
try:
agent = await agent_repo.create_agent(
data,
actor_id=actor.get("actor_id")
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# Log event
await event_repo.log_event(
agent.external_id,
"created",
payload={
"name": agent.name,
"kind": agent.kind.value,
"blueprint_id": agent.blueprint_id,
"created_by": actor.get("actor_id")
}
)
return agent
# ============================================================================
# CRUD — Read
# ============================================================================
@router.get("", response_model=List[AgentRead])
async def list_agents(
microdao_id: Optional[str] = None,
kind: Optional[str] = None,
is_active: bool = True
):
"""
List agents with filters
For now, no auth required (read-only)
TODO: Filter by actor's microdao_ids in Phase 6.5
"""
return await agent_repo.list_agents(
microdao_id=microdao_id,
kind=kind,
is_active=is_active
)
@router.get("/{agent_id}", response_model=AgentRead)
async def get_agent(agent_id: str):
"""
Get agent by ID
For now, no auth required (read-only)
TODO: PDP check in Phase 6.5
"""
agent = await agent_repo.get_agent_by_external_id(agent_id)
if not agent:
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
return agent
# ============================================================================
# CRUD — Update
# ============================================================================
@router.patch("/{agent_id}", response_model=AgentRead)
async def update_agent(
agent_id: str,
data: AgentUpdate,
actor: dict = Depends(get_actor_from_token)
):
"""
Update agent
Requires: MANAGE permission on AGENT:{agent_id}
"""
# Check agent exists
existing = await agent_repo.get_agent_by_external_id(agent_id)
if not existing:
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
# Check PDP
allowed = await check_pdp_permission(
action="MANAGE",
resource={"type": "AGENT", "id": agent_id},
context={"operation": "update"},
actor=actor
)
if not allowed:
raise HTTPException(status_code=403, detail="Permission denied: cannot update agent")
# Update
try:
agent = await agent_repo.update_agent(agent_id, data)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# Log event
changed_fields = {k: v for k, v in data.dict(exclude_unset=True).items()}
await event_repo.log_event(
agent_id,
"updated",
payload={
"changed_fields": changed_fields,
"updated_by": actor.get("actor_id")
}
)
return agent
# ============================================================================
# CRUD — Delete
# ============================================================================
@router.delete("/{agent_id}", status_code=204)
async def delete_agent(
agent_id: str,
actor: dict = Depends(get_actor_from_token)
):
"""
Delete (deactivate) agent
Requires: MANAGE permission on AGENT:{agent_id}
"""
# Check agent exists
existing = await agent_repo.get_agent_by_external_id(agent_id)
if not existing:
raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' not found")
# Check PDP
allowed = await check_pdp_permission(
action="MANAGE",
resource={"type": "AGENT", "id": agent_id},
context={"operation": "delete"},
actor=actor
)
if not allowed:
raise HTTPException(status_code=403, detail="Permission denied: cannot delete agent")
# Soft delete
try:
await agent_repo.delete_agent(agent_id)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
# Log event
await event_repo.log_event(
agent_id,
"deleted",
payload={
"deleted_by": actor.get("actor_id")
}
)
return None