- 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
265 lines
7.8 KiB
Python
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
|
|
|