from __future__ import annotations from fastapi import APIRouter, Depends, Header, HTTPException, Query from pydantic import BaseModel, ConfigDict, Field from sqlmodel import Session from ..core.config import settings from ..core.security import n8n_auth, operator_auth from ..db.session import get_session from ..mcp_server import runtime_status as mcp_runtime_status from ..services import knowledge_service, n8n_integration router = APIRouter(prefix="/api/integrations") n8n_router = APIRouter(prefix="/api/integrations/n8n", dependencies=[Depends(n8n_auth)]) class PreflightRequest(BaseModel): model_config = ConfigDict(extra="forbid") action: str = Field(min_length=1, max_length=160) target: str = Field(min_length=1, max_length=240) class ApprovalRequest(BaseModel): model_config = ConfigDict(extra="forbid") action: str = Field(min_length=1, max_length=160) target: str = Field(min_length=1, max_length=240) reason: str = Field(min_length=3, max_length=1000) plan: list[str] = Field(default_factory=list, max_length=30) class KnowledgeRequest(BaseModel): model_config = ConfigDict(extra="forbid") question: str = Field(min_length=2, max_length=500) limit: int = Field(default=6, ge=1, le=10) class HeartbeatRequest(BaseModel): model_config = ConfigDict(extra="forbid") workflowId: str = Field(min_length=1, max_length=120) workflowVersion: str = Field(default="1", max_length=40) branch: str = Field(default="unknown", max_length=40) processedEvents: int = Field(default=0, ge=0, le=100_000) backlogRemaining: bool = False confidence: str = Field(default="", max_length=20) @router.get("/status", dependencies=[Depends(operator_auth)]) def status(session: Session = Depends(get_session)): rag = knowledge_service.index_status(session) mcp_runtime = mcp_runtime_status() n8n_runtime = n8n_integration.heartbeat_status(session) return { "mcp": { "enabled": settings.mcp_enabled, "authenticated": bool(settings.mcp_token), "transport": "streamable_http", "endpoint": "/mcp", "scope": "governed_operations", "canRequestApproval": True, "canExecuteApproved": True, "requiresHumanApproval": True, "maxConcurrentExecutions": 1, **mcp_runtime, }, "rag": rag, "n8n": { "enabled": settings.n8n_enabled, "authenticated": bool(settings.n8n_token), "endpoint": "/api/integrations/n8n", "canExecute": False, "capabilities": [ "event_feed", "knowledge_query", "preflight", "create_approval", "read_approval", "read_job", ], **n8n_runtime, }, } @n8n_router.post("/heartbeat") def heartbeat(payload: HeartbeatRequest, session: Session = Depends(get_session)): return n8n_integration.record_heartbeat(session, payload.model_dump()) @n8n_router.get("/events") def events( cursor: str | None = Query(default=None, max_length=600), limit: int = Query(default=100, ge=1, le=250), session: Session = Depends(get_session), ): try: return n8n_integration.event_feed(session, cursor=cursor, limit=limit) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc @n8n_router.post("/knowledge/query") def knowledge_query(payload: KnowledgeRequest, session: Session = Depends(get_session)): try: return knowledge_service.query(session, payload.question, limit=payload.limit) except ValueError as exc: raise HTTPException(status_code=422, detail=str(exc)) from exc @n8n_router.post("/preflight") def preflight(payload: PreflightRequest, session: Session = Depends(get_session)): return n8n_integration.preflight(session, payload.action, payload.target) @n8n_router.post("/approvals") def create_approval( payload: ApprovalRequest, idempotency_key: str = Header( alias="Idempotency-Key", min_length=16, max_length=128 ), session: Session = Depends(get_session), ): try: return n8n_integration.request_approval( session, action=payload.action, target=payload.target, reason=payload.reason, plan=payload.plan, idempotency_key=idempotency_key, ) except PermissionError as exc: raise HTTPException(status_code=403, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc @n8n_router.get("/approvals/{approval_id}") def approval(approval_id: str, session: Session = Depends(get_session)): result = n8n_integration.approval_status(session, approval_id) if not result: raise HTTPException(status_code=404, detail="approval_not_found") return result @n8n_router.get("/jobs/{job_id}") def job(job_id: str, session: Session = Depends(get_session)): result = n8n_integration.job_status(session, job_id) if not result: raise HTTPException(status_code=404, detail="job_not_found") return result