M13: harden MCP trust boundary
This commit is contained in:
@@ -3,11 +3,11 @@ from __future__ import annotations
|
||||
import uuid
|
||||
from datetime import date
|
||||
|
||||
from fastapi import APIRouter, Depends, Header, Query
|
||||
from fastapi import APIRouter, Depends, Header, Query, Response
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db, require_mcp_service_token
|
||||
from app.api.deps import McpClientContext, get_db, require_mcp_service_token
|
||||
from app.core.config import get_settings
|
||||
from app.core.errors import AppError
|
||||
from app.models.booking import Booking
|
||||
@@ -42,7 +42,8 @@ def get_correlation_id(
|
||||
|
||||
|
||||
def _audit_service_request(
|
||||
db: Session, *, client_id: str, tool: str, status_label: str, correlation_id: str
|
||||
db: Session, *, client_id: str, tool: str, status_label: str, correlation_id: str,
|
||||
metadata: dict[str, object] | None = None,
|
||||
) -> None:
|
||||
record_audit_event(
|
||||
db,
|
||||
@@ -51,43 +52,53 @@ def _audit_service_request(
|
||||
action="mcp_tool_request",
|
||||
entity_type="mcp_tool",
|
||||
correlation_id=uuid.UUID(correlation_id),
|
||||
metadata={"tool": tool, "status": status_label},
|
||||
metadata={"tool": tool, "status": status_label, **(metadata or {})},
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
def _set_trace_headers(response: Response, correlation_id: str, tenant: str) -> None:
|
||||
response.headers["X-Correlation-Id"] = correlation_id
|
||||
response.headers["X-Tenant-Id"] = tenant
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
|
||||
|
||||
@router.get("/operations-summary", response_model=OperationsSummaryOut)
|
||||
def operations_summary(
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
client_id: str = Depends(require_mcp_service_token),
|
||||
client: McpClientContext = Depends(require_mcp_service_token),
|
||||
correlation_id: str = Depends(get_correlation_id),
|
||||
) -> OperationsSummaryOut:
|
||||
metrics = compute_metrics(db)
|
||||
_set_trace_headers(response, correlation_id, client.tenant)
|
||||
_audit_service_request(
|
||||
db,
|
||||
client_id=client_id,
|
||||
client_id=client.client_id,
|
||||
tool="fleet_ops_get_operations_summary",
|
||||
status_label="ok",
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
return OperationsSummaryOut(tenant=settings.ragcore_tenant, metrics=metrics)
|
||||
return OperationsSummaryOut(tenant=client.tenant, metrics=metrics)
|
||||
|
||||
|
||||
@router.get("/attention-vehicles", response_model=list[AttentionVehicleOut])
|
||||
def attention_vehicles(
|
||||
response: Response,
|
||||
minimum_severity: str = Query(default="medium", pattern="^(low|medium|high)$"),
|
||||
date_filter: date | None = Query(default=None, alias="date"),
|
||||
limit: int = Query(default=20, ge=1, le=50),
|
||||
db: Session = Depends(get_db),
|
||||
client_id: str = Depends(require_mcp_service_token),
|
||||
client: McpClientContext = Depends(require_mcp_service_token),
|
||||
correlation_id: str = Depends(get_correlation_id),
|
||||
) -> list[AttentionVehicleOut]:
|
||||
results = list_attention_vehicles(
|
||||
db, minimum_severity=minimum_severity, on_or_before=date_filter, limit=limit
|
||||
)
|
||||
_set_trace_headers(response, correlation_id, client.tenant)
|
||||
_audit_service_request(
|
||||
db,
|
||||
client_id=client_id,
|
||||
client_id=client.client_id,
|
||||
tool="fleet_ops_list_attention_vehicles",
|
||||
status_label="ok",
|
||||
correlation_id=correlation_id,
|
||||
@@ -98,15 +109,17 @@ def attention_vehicles(
|
||||
@router.get("/vehicles/{vehicle_ref}", response_model=McpVehicleDetailOut)
|
||||
def vehicle_details(
|
||||
vehicle_ref: str,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
client_id: str = Depends(require_mcp_service_token),
|
||||
client: McpClientContext = Depends(require_mcp_service_token),
|
||||
correlation_id: str = Depends(get_correlation_id),
|
||||
) -> McpVehicleDetailOut:
|
||||
_set_trace_headers(response, correlation_id, client.tenant)
|
||||
vehicle = db.scalar(select(Vehicle).where(Vehicle.public_ref == vehicle_ref))
|
||||
if vehicle is None:
|
||||
_audit_service_request(
|
||||
db,
|
||||
client_id=client_id,
|
||||
client_id=client.client_id,
|
||||
tool="fleet_ops_get_vehicle_details",
|
||||
status_label="not_found",
|
||||
correlation_id=correlation_id,
|
||||
@@ -128,7 +141,7 @@ def vehicle_details(
|
||||
|
||||
_audit_service_request(
|
||||
db,
|
||||
client_id=client_id,
|
||||
client_id=client.client_id,
|
||||
tool="fleet_ops_get_vehicle_details",
|
||||
status_label="ok",
|
||||
correlation_id=correlation_id,
|
||||
@@ -150,18 +163,29 @@ def vehicle_details(
|
||||
@router.post("/search-knowledge", response_model=GroundedAnswer)
|
||||
def search_knowledge(
|
||||
body: McpKnowledgeSearchRequest,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
client_id: str = Depends(require_mcp_service_token),
|
||||
client: McpClientContext = Depends(require_mcp_service_token),
|
||||
correlation_id: str = Depends(get_correlation_id),
|
||||
) -> GroundedAnswer:
|
||||
provider = get_knowledge_provider()
|
||||
answer = provider.ask(body.question, correlation_id, language=body.locale)
|
||||
source_count_available = len(answer.sources)
|
||||
answer.sources = answer.sources[: body.max_sources]
|
||||
_set_trace_headers(response, correlation_id, client.tenant)
|
||||
response.headers["X-Sources-Available"] = str(source_count_available)
|
||||
response.headers["X-Sources-Returned"] = str(len(answer.sources))
|
||||
_audit_service_request(
|
||||
db,
|
||||
client_id=client_id,
|
||||
client_id=client.client_id,
|
||||
tool="fleet_ops_search_knowledge",
|
||||
status_label=answer.evidence_state,
|
||||
correlation_id=correlation_id,
|
||||
metadata={
|
||||
"tenant": client.tenant,
|
||||
"locale": body.locale,
|
||||
"sources_available": source_count_available,
|
||||
"sources_returned": len(answer.sources),
|
||||
},
|
||||
)
|
||||
return answer
|
||||
|
||||
Reference in New Issue
Block a user