Implement complete agent platform with four major layers:
P5 - Engine Layer (LangGraph):
- AgentState with JSON serialization validation
- 5 async nodes (ask, plan, act, verify, report) with error boundaries
- Template resolution system for {{user_input}} and {{steps.xxx}}
- StateGraph with conditional edges and interrupt_after for approval flow
- 17/17 tests passing, 90% coverage
P6 - LLM Layer:
- ClaudeClient with Decimal-based cost calculation (ROUND_HALF_UP)
- Custom exceptions (ClaudeAPIError, ClaudeTimeoutError, ClaudeRateLimitError)
- PromptBuilder with cache_control support
- ModelRouter with sensitivity-based routing (local vs Claude)
- 18/18 tests passing, 90% coverage
P7 - Audit Layer (Three-tier):
- Sanitizer: mask phone/ID card/API keys, contains_sensitive, mask_dict
- Encryptor: AES-256-GCM with keyring support, 12-byte IV
- AuditLogger: L1 (DB), L2 (OTel), L3 (Encrypted payloads)
- 24/24 tests passing, 86% coverage
P8 - API Layer (FastAPI):
- Lifespan management with 8-step startup sequence
- Middleware: TracingMiddleware, RateLimitMiddleware (Redis), CORS
- Auth: JWT decode with python-jose, RBAC with permission matrix
- Routes: health, chat (SSE), approve, trace
- 13/13 tests passing, 91% coverage
All gate checks passing:
- pytest with 85%+ coverage
- mypy type checking
- ruff style checking
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
70 lines
1.8 KiB
Python
70 lines
1.8 KiB
Python
"""
|
|
JWT authentication middleware.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import Depends, HTTPException, status
|
|
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
|
from jose import JWTError, jwt
|
|
|
|
from api.contracts import UserContext
|
|
from core.config import get_settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
security = HTTPBearer()
|
|
|
|
|
|
async def get_current_user(
|
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
|
) -> UserContext:
|
|
"""
|
|
Decode JWT token and extract user context.
|
|
|
|
Args:
|
|
credentials: HTTP Bearer credentials
|
|
|
|
Returns:
|
|
UserContext with user info
|
|
|
|
Raises:
|
|
HTTPException: 401 if token invalid
|
|
"""
|
|
token = credentials.credentials
|
|
settings = get_settings()
|
|
|
|
try:
|
|
# Decode JWT
|
|
payload: dict[str, Any] = jwt.decode(
|
|
token,
|
|
settings.JWT_SECRET_KEY,
|
|
algorithms=[settings.JWT_ALGORITHM],
|
|
)
|
|
|
|
# Extract user context
|
|
user_context = UserContext(
|
|
user_id=payload["sub"],
|
|
tenant_id=payload.get("tenant_id", "default"),
|
|
role=payload.get("role", "user"),
|
|
data_scope=payload.get("data_scope", "tenant"),
|
|
token_exp=payload.get("exp", 0),
|
|
)
|
|
|
|
logger.debug(f"Authenticated user: {user_context.user_id}")
|
|
return user_context
|
|
|
|
except jwt.ExpiredSignatureError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Token expired",
|
|
)
|
|
except JWTError as e:
|
|
logger.warning(f"Invalid JWT token: {e}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid token",
|
|
)
|