Files
app/backend/app/api/deps.py
T
stroblmeandClaude Fable 5 f300c43f3a Run python nodes out of process, with modules of their own
User code no longer execs in the engine. A pool of persistent worker
subprocesses speaks one JSON object per line; the controller installs a
proxy as the node's function, so every execution path funnels through it
and the pipeline is untouched. A crash costs one subprocess, a per-node
timeout is a kill, and cancelling from the canvas is that same kill.

The workers run a venv of the user's own on the data volume, filled from
a pip manifest versioned beside the flows. Applying it retires the
workers and rebuilds, so a package lands without restarting the engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017MeiWk3Yq12n2pTvnQWYvt
2026-08-16 21:43:36 +02:00

133 lines
4.4 KiB
Python

from collections.abc import Generator
from typing import Annotated, Any
import jwt
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import OAuth2PasswordBearer
from jwt.exceptions import InvalidTokenError
from pydantic import ValidationError
from sqlmodel import Session
from app.core import security
from app.core.config import settings
from app.core.db import engine
from app.flow.controller import FlowController
from app.flow.dashboards import DashboardStore
from app.flow.workers import PythonWorkerPool
from app.models import TokenPayload, User
reusable_oauth2 = OAuth2PasswordBearer(
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
)
def get_db() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
SessionDep = Annotated[Session, Depends(get_db)]
TokenDep = Annotated[str, Depends(reusable_oauth2)]
def decode_token(token: str) -> dict[str, Any]:
"""Read a bearer token, whichever channel issued it.
A browser session is signed with the app's own secret; an agent's token is
signed with the OAuth keypair, so that set can be revoked on its own and
the public half published. Both name a user, and both grant that user's
rights — the difference is only in who is holding it, which the MCP
endpoint checks separately.
This is also the seam a hosted deployment widens later: trusting an
additional issuer is a third branch here, not a change anywhere else.
"""
try:
session: dict[str, Any] = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
return session
except InvalidTokenError:
return security.decode_oauth_token(token)
def user_from_token(session: Session, token: str) -> User | None:
"""Resolve a bearer token to its user, or None if it does not hold up.
Shared with the websocket, which cannot use the HTTP security scheme.
"""
try:
token_data = TokenPayload(**decode_token(token))
except (InvalidTokenError, ValidationError):
return None
user = session.get(User, token_data.sub)
if user is None or not user.is_active:
return None
return user
def get_current_user(session: SessionDep, token: TokenDep) -> User:
"""Resolve the bearer token to its user.
Every failure here is an authentication failure, so all of them answer 401:
a token naming a user who no longer exists is a session to log in again,
not a missing resource to report.
"""
try:
token_data = TokenPayload(**decode_token(token))
except (InvalidTokenError, ValidationError):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
)
user = session.get(User, token_data.sub)
if user is None or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="This session is no longer valid — please log in again",
)
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
def get_flow_controller(request: Request) -> FlowController:
controller: FlowController | None = getattr(
request.app.state, "flow_controller", None
)
if controller is None:
raise HTTPException(status_code=503, detail="The flow engine is not running")
return controller
FlowControllerDep = Annotated[FlowController, Depends(get_flow_controller)]
def get_dashboard_store(request: Request) -> DashboardStore:
store: DashboardStore | None = getattr(request.app.state, "dashboard_store", None)
if store is None:
raise HTTPException(status_code=503, detail="The flow engine is not running")
return store
DashboardStoreDep = Annotated[DashboardStore, Depends(get_dashboard_store)]
def get_worker_pool(request: Request) -> PythonWorkerPool:
pool: PythonWorkerPool | None = getattr(request.app.state, "worker_pool", None)
if pool is None:
raise HTTPException(status_code=503, detail="The flow engine is not running")
return pool
WorkerPoolDep = Annotated[PythonWorkerPool, Depends(get_worker_pool)]
def get_current_active_superuser(current_user: CurrentUser) -> User:
if not current_user.is_superuser:
raise HTTPException(
status_code=403, detail="The user doesn't have enough privileges"
)
return current_user