fix(nginx): defer CORS to gateway allowlist (#2861)
* fix(nginx): defer cors to gateway allowlist Remove proxy-level wildcard CORS handling so browser origins are controlled by the Gateway allowlist and stay aligned with CSRF origin checks. * docs: document gateway cors allowlist Clarify that same-origin nginx access needs no CORS headers while split-origin or port-forwarded browser clients must opt in with GATEWAY_CORS_ORIGINS. * docs(gateway): record cors source of truth Document that Gateway CORSMiddleware and CSRFMiddleware share GATEWAY_CORS_ORIGINS as the split-origin source of truth. * fix(gateway): align cors origin normalization * docs: clarify gateway langgraph routing * docs(gateway): update runtime routing note
This commit is contained in:
+3
-2
@@ -9,8 +9,9 @@ JINA_API_KEY=your-jina-api-key
|
|||||||
|
|
||||||
# InfoQuest API Key
|
# InfoQuest API Key
|
||||||
INFOQUEST_API_KEY=your-infoquest-api-key
|
INFOQUEST_API_KEY=your-infoquest-api-key
|
||||||
# CORS Origins (comma-separated) - e.g., http://localhost:3000,http://localhost:3001
|
# Browser CORS allowlist for split-origin or port-forwarded deployments (comma-separated exact origins).
|
||||||
# CORS_ORIGINS=http://localhost:3000
|
# Leave unset when using the unified nginx endpoint, e.g. http://localhost:2026.
|
||||||
|
# GATEWAY_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
||||||
|
|
||||||
# Optional:
|
# Optional:
|
||||||
# FIRECRAWL_API_KEY=your-firecrawl-api-key
|
# FIRECRAWL_API_KEY=your-firecrawl-api-key
|
||||||
|
|||||||
+13
-19
@@ -46,12 +46,12 @@ Docker provides a consistent, isolated environment with all dependencies pre-con
|
|||||||
All services will start with hot-reload enabled:
|
All services will start with hot-reload enabled:
|
||||||
- Frontend changes are automatically reloaded
|
- Frontend changes are automatically reloaded
|
||||||
- Backend changes trigger automatic restart
|
- Backend changes trigger automatic restart
|
||||||
- LangGraph server supports hot-reload
|
- Gateway-hosted LangGraph-compatible runtime supports hot-reload
|
||||||
|
|
||||||
4. **Access the application**:
|
4. **Access the application**:
|
||||||
- Web Interface: http://localhost:2026
|
- Web Interface: http://localhost:2026
|
||||||
- API Gateway: http://localhost:2026/api/*
|
- API Gateway: http://localhost:2026/api/*
|
||||||
- LangGraph: http://localhost:2026/api/langgraph/*
|
- LangGraph-compatible API: http://localhost:2026/api/langgraph/*
|
||||||
|
|
||||||
#### Docker Commands
|
#### Docker Commands
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ Use these as practical starting points for development and review environments:
|
|||||||
If `make docker-init`, `make docker-start`, or `make docker-stop` fails on Linux with an error like below, your current user likely does not have permission to access the Docker daemon socket:
|
If `make docker-init`, `make docker-start`, or `make docker-stop` fails on Linux with an error like below, your current user likely does not have permission to access the Docker daemon socket:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
unable to get image 'deer-flow-dev-langgraph': permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
|
unable to get image 'deer-flow-gateway': permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
|
||||||
```
|
```
|
||||||
|
|
||||||
Recommended fix: add your current user to the `docker` group so Docker commands work without `sudo`.
|
Recommended fix: add your current user to the `docker` group so Docker commands work without `sudo`.
|
||||||
@@ -131,9 +131,8 @@ Host Machine
|
|||||||
Docker Compose (deer-flow-dev)
|
Docker Compose (deer-flow-dev)
|
||||||
├→ nginx (port 2026) ← Reverse proxy
|
├→ nginx (port 2026) ← Reverse proxy
|
||||||
├→ web (port 3000) ← Frontend with hot-reload
|
├→ web (port 3000) ← Frontend with hot-reload
|
||||||
├→ api (port 8001) ← Gateway API with hot-reload
|
├→ gateway (port 8001) ← Gateway API + LangGraph-compatible runtime with hot-reload
|
||||||
├→ langgraph (port 2024) ← LangGraph server with hot-reload
|
└→ provisioner (optional, port 8002) ← Started only in provisioner/K8s sandbox mode
|
||||||
└→ provisioner (optional, port 8002) ← Started only in provisioner/K8s sandbox mode
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Benefits of Docker Development**:
|
**Benefits of Docker Development**:
|
||||||
@@ -184,17 +183,13 @@ Required tools:
|
|||||||
|
|
||||||
If you need to start services individually:
|
If you need to start services individually:
|
||||||
|
|
||||||
1. **Start backend services**:
|
1. **Start backend service**:
|
||||||
```bash
|
```bash
|
||||||
# Terminal 1: Start LangGraph Server (port 2024)
|
# Terminal 1: Start Gateway API and embedded LangGraph-compatible runtime (port 8001)
|
||||||
cd backend
|
|
||||||
make dev
|
|
||||||
|
|
||||||
# Terminal 2: Start Gateway API (port 8001)
|
|
||||||
cd backend
|
cd backend
|
||||||
make gateway
|
make gateway
|
||||||
|
|
||||||
# Terminal 3: Start Frontend (port 3000)
|
# Terminal 2: Start Frontend (port 3000)
|
||||||
cd frontend
|
cd frontend
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
@@ -212,10 +207,10 @@ If you need to start services individually:
|
|||||||
|
|
||||||
The nginx configuration provides:
|
The nginx configuration provides:
|
||||||
- Unified entry point on port 2026
|
- Unified entry point on port 2026
|
||||||
- Routes `/api/langgraph/*` to LangGraph Server (2024)
|
- Gateway owns `/api/langgraph/*` and translates those public LangGraph-compatible paths to its native `/api/*` routers behind nginx
|
||||||
- Routes other `/api/*` endpoints to Gateway API (8001)
|
- Routes other `/api/*` endpoints to Gateway API (8001)
|
||||||
- Routes non-API requests to Frontend (3000)
|
- Routes non-API requests to Frontend (3000)
|
||||||
- Centralized CORS handling
|
- Same-origin API routing; split-origin or port-forwarded browser clients should use the Gateway `GATEWAY_CORS_ORIGINS` allowlist
|
||||||
- SSE/streaming support for real-time agent responses
|
- SSE/streaming support for real-time agent responses
|
||||||
- Optimized timeouts for long-running operations
|
- Optimized timeouts for long-running operations
|
||||||
|
|
||||||
@@ -235,8 +230,8 @@ deer-flow/
|
|||||||
│ └── nginx.local.conf # Nginx config for local dev
|
│ └── nginx.local.conf # Nginx config for local dev
|
||||||
├── backend/ # Backend application
|
├── backend/ # Backend application
|
||||||
│ ├── src/
|
│ ├── src/
|
||||||
│ │ ├── gateway/ # Gateway API (port 8001)
|
│ │ ├── gateway/ # Gateway API and LangGraph-compatible runtime (port 8001)
|
||||||
│ │ ├── agents/ # LangGraph agents (port 2024)
|
│ │ ├── agents/ # LangGraph agent definitions
|
||||||
│ │ ├── mcp/ # Model Context Protocol integration
|
│ │ ├── mcp/ # Model Context Protocol integration
|
||||||
│ │ ├── skills/ # Skills system
|
│ │ ├── skills/ # Skills system
|
||||||
│ │ └── sandbox/ # Sandbox execution
|
│ │ └── sandbox/ # Sandbox execution
|
||||||
@@ -256,8 +251,7 @@ Browser
|
|||||||
↓
|
↓
|
||||||
Nginx (port 2026) ← Unified entry point
|
Nginx (port 2026) ← Unified entry point
|
||||||
├→ Frontend (port 3000) ← / (non-API requests)
|
├→ Frontend (port 3000) ← / (non-API requests)
|
||||||
├→ Gateway API (port 8001) ← /api/models, /api/mcp, /api/skills, /api/threads/*/artifacts
|
└→ Gateway API (port 8001) ← /api/* and /api/langgraph/* (LangGraph-compatible agent interactions)
|
||||||
└→ LangGraph Server (port 2024) ← /api/langgraph/* (agent interactions)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Workflow
|
## Development Workflow
|
||||||
|
|||||||
@@ -245,6 +245,8 @@ make down # Stop and remove containers
|
|||||||
|
|
||||||
Access: http://localhost:2026
|
Access: http://localhost:2026
|
||||||
|
|
||||||
|
The unified nginx endpoint is same-origin by default and does not emit browser CORS headers. If you run a split-origin or port-forwarded browser client, set `GATEWAY_CORS_ORIGINS` to comma-separated exact origins such as `http://localhost:3000`; the Gateway then applies the CORS allowlist and matching CSRF origin checks.
|
||||||
|
|
||||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed Docker development guide.
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed Docker development guide.
|
||||||
|
|
||||||
#### Option 2: Local Development
|
#### Option 2: Local Development
|
||||||
|
|||||||
+3
-1
@@ -207,6 +207,8 @@ Configuration priority:
|
|||||||
|
|
||||||
FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled).
|
FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled).
|
||||||
|
|
||||||
|
CORS is same-origin by default when requests enter through nginx on port 2026. Split-origin or port-forwarded browser clients must opt in with `GATEWAY_CORS_ORIGINS` (comma-separated exact origins); Gateway `CORSMiddleware` and `CSRFMiddleware` both read that variable so browser CORS and auth-origin checks stay aligned.
|
||||||
|
|
||||||
**Routers**:
|
**Routers**:
|
||||||
|
|
||||||
| Router | Endpoints |
|
| Router | Endpoints |
|
||||||
@@ -223,7 +225,7 @@ FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWA
|
|||||||
| **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific |
|
| **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific |
|
||||||
| **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id |
|
| **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id |
|
||||||
|
|
||||||
Proxied through nginx: `/api/langgraph/*` → LangGraph, all other `/api/*` → Gateway.
|
Proxied through nginx: `/api/langgraph/*` → Gateway LangGraph-compatible runtime, all other `/api/*` → Gateway REST APIs.
|
||||||
|
|
||||||
### Sandbox System (`packages/harness/deerflow/sandbox/`)
|
### Sandbox System (`packages/harness/deerflow/sandbox/`)
|
||||||
|
|
||||||
|
|||||||
+22
-19
@@ -14,28 +14,31 @@ DeerFlow is a LangGraph-based AI super agent with sandbox execution, persistent
|
|||||||
│ │
|
│ │
|
||||||
/api/langgraph/* │ │ /api/* (other)
|
/api/langgraph/* │ │ /api/* (other)
|
||||||
▼ ▼
|
▼ ▼
|
||||||
┌────────────────────┐ ┌────────────────────────┐
|
┌──────────────────────────────────────────────┐
|
||||||
│ LangGraph Server │ │ Gateway API (8001) │
|
│ Gateway API (8001) │
|
||||||
│ (Port 2024) │ │ FastAPI REST │
|
│ FastAPI REST + LangGraph-compatible runtime │
|
||||||
│ │ │ │
|
│ │
|
||||||
│ ┌────────────────┐ │ │ Models, MCP, Skills, │
|
│ Models, MCP, Skills, Memory, Uploads, │
|
||||||
│ │ Lead Agent │ │ │ Memory, Uploads, │
|
│ Artifacts, Threads, Runs, Streaming │
|
||||||
│ │ ┌──────────┐ │ │ │ Artifacts │
|
│ │
|
||||||
│ │ │Middleware│ │ │ └────────────────────────┘
|
│ ┌────────────────┐ │
|
||||||
│ │ │ Chain │ │ │
|
│ │ Lead Agent │ │
|
||||||
│ │ └──────────┘ │ │
|
│ │ ┌──────────┐ │ │
|
||||||
│ │ ┌──────────┐ │ │
|
│ │ │Middleware│ │ │
|
||||||
│ │ │ Tools │ │ │
|
│ │ │ Chain │ │ │
|
||||||
│ │ └──────────┘ │ │
|
│ │ └──────────┘ │ │
|
||||||
│ │ ┌──────────┐ │ │
|
│ │ ┌──────────┐ │ │
|
||||||
│ │ │Subagents │ │ │
|
│ │ │ Tools │ │ │
|
||||||
│ │ └──────────┘ │ │
|
│ │ └──────────┘ │ │
|
||||||
│ └────────────────┘ │
|
│ │ ┌──────────┐ │ │
|
||||||
└────────────────────┘
|
│ │ │Subagents │ │ │
|
||||||
|
│ │ └──────────┘ │ │
|
||||||
|
│ └────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**Request Routing** (via Nginx):
|
**Request Routing** (via Nginx):
|
||||||
- `/api/langgraph/*` → LangGraph Server - agent interactions, threads, streaming
|
- `/api/langgraph/*` → Gateway API - LangGraph-compatible agent interactions, threads, runs, and streaming translated to native `/api/*` routers
|
||||||
- `/api/*` (other) → Gateway API - models, MCP, skills, memory, artifacts, uploads, thread-local cleanup
|
- `/api/*` (other) → Gateway API - models, MCP, skills, memory, artifacts, uploads, thread-local cleanup
|
||||||
- `/` (non-API) → Frontend - Next.js web interface
|
- `/` (non-API) → Frontend - Next.js web interface
|
||||||
|
|
||||||
|
|||||||
+22
-26
@@ -1,6 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
@@ -9,7 +8,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
|||||||
|
|
||||||
from app.gateway.auth_middleware import AuthMiddleware
|
from app.gateway.auth_middleware import AuthMiddleware
|
||||||
from app.gateway.config import get_gateway_config
|
from app.gateway.config import get_gateway_config
|
||||||
from app.gateway.csrf_middleware import CSRFMiddleware
|
from app.gateway.csrf_middleware import CSRFMiddleware, get_configured_cors_origins
|
||||||
from app.gateway.deps import langgraph_runtime
|
from app.gateway.deps import langgraph_runtime
|
||||||
from app.gateway.routers import (
|
from app.gateway.routers import (
|
||||||
agents,
|
agents,
|
||||||
@@ -219,7 +218,9 @@ def create_app() -> FastAPI:
|
|||||||
Configured FastAPI application instance.
|
Configured FastAPI application instance.
|
||||||
"""
|
"""
|
||||||
config = get_gateway_config()
|
config = get_gateway_config()
|
||||||
docs_kwargs = {"docs_url": "/docs", "redoc_url": "/redoc", "openapi_url": "/openapi.json"} if config.enable_docs else {"docs_url": None, "redoc_url": None, "openapi_url": None}
|
docs_url = "/docs" if config.enable_docs else None
|
||||||
|
redoc_url = "/redoc" if config.enable_docs else None
|
||||||
|
openapi_url = "/openapi.json" if config.enable_docs else None
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="DeerFlow API Gateway",
|
title="DeerFlow API Gateway",
|
||||||
@@ -239,12 +240,14 @@ API Gateway for DeerFlow - A LangGraph-based AI agent backend with sandbox execu
|
|||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
LangGraph requests are handled by nginx reverse proxy.
|
LangGraph-compatible requests are routed through nginx to this gateway.
|
||||||
This gateway provides custom endpoints for models, MCP configuration, skills, and artifacts.
|
This gateway provides runtime endpoints for agent runs plus custom endpoints for models, MCP configuration, skills, and artifacts.
|
||||||
""",
|
""",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
**docs_kwargs,
|
docs_url=docs_url,
|
||||||
|
redoc_url=redoc_url,
|
||||||
|
openapi_url=openapi_url,
|
||||||
openapi_tags=[
|
openapi_tags=[
|
||||||
{
|
{
|
||||||
"name": "models",
|
"name": "models",
|
||||||
@@ -307,25 +310,18 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
|
|||||||
# CSRF: Double Submit Cookie pattern for state-changing requests
|
# CSRF: Double Submit Cookie pattern for state-changing requests
|
||||||
app.add_middleware(CSRFMiddleware)
|
app.add_middleware(CSRFMiddleware)
|
||||||
|
|
||||||
# CORS: when GATEWAY_CORS_ORIGINS is set (dev without nginx), add CORS middleware.
|
# CORS: the unified nginx endpoint is same-origin by default. Split-origin
|
||||||
# In production, nginx handles CORS and no middleware is needed.
|
# browser clients must opt in with this explicit Gateway allowlist so CORS
|
||||||
cors_origins_env = os.environ.get("GATEWAY_CORS_ORIGINS", "")
|
# and CSRF origin checks share the same source of truth.
|
||||||
if cors_origins_env:
|
cors_origins = sorted(get_configured_cors_origins())
|
||||||
cors_origins = [o.strip() for o in cors_origins_env.split(",") if o.strip()]
|
if cors_origins:
|
||||||
# Validate: wildcard origin with credentials is a security misconfiguration
|
app.add_middleware(
|
||||||
for origin in cors_origins:
|
CORSMiddleware,
|
||||||
if origin == "*":
|
allow_origins=cors_origins,
|
||||||
logger.error("GATEWAY_CORS_ORIGINS contains wildcard '*' with allow_credentials=True. This is a security misconfiguration — browsers will reject the response. Use explicit scheme://host:port origins instead.")
|
allow_credentials=True,
|
||||||
cors_origins = [o for o in cors_origins if o != "*"]
|
allow_methods=["*"],
|
||||||
break
|
allow_headers=["*"],
|
||||||
if cors_origins:
|
)
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=cors_origins,
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include routers
|
# Include routers
|
||||||
# Models API is mounted at /api/models
|
# Models API is mounted at /api/models
|
||||||
@@ -374,7 +370,7 @@ This gateway provides custom endpoints for models, MCP configuration, skills, an
|
|||||||
app.include_router(runs.router)
|
app.include_router(runs.router)
|
||||||
|
|
||||||
@app.get("/health", tags=["health"])
|
@app.get("/health", tags=["health"])
|
||||||
async def health_check() -> dict:
|
async def health_check() -> dict[str, str]:
|
||||||
"""Health check endpoint.
|
"""Health check endpoint.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ class GatewayConfig(BaseModel):
|
|||||||
|
|
||||||
host: str = Field(default="0.0.0.0", description="Host to bind the gateway server")
|
host: str = Field(default="0.0.0.0", description="Host to bind the gateway server")
|
||||||
port: int = Field(default=8001, description="Port to bind the gateway server")
|
port: int = Field(default=8001, description="Port to bind the gateway server")
|
||||||
cors_origins: list[str] = Field(default_factory=lambda: ["http://localhost:3000"], description="Allowed CORS origins")
|
|
||||||
enable_docs: bool = Field(default=True, description="Enable Swagger/ReDoc/OpenAPI endpoints")
|
enable_docs: bool = Field(default=True, description="Enable Swagger/ReDoc/OpenAPI endpoints")
|
||||||
|
|
||||||
|
|
||||||
@@ -19,11 +18,9 @@ def get_gateway_config() -> GatewayConfig:
|
|||||||
"""Get gateway config, loading from environment if available."""
|
"""Get gateway config, loading from environment if available."""
|
||||||
global _gateway_config
|
global _gateway_config
|
||||||
if _gateway_config is None:
|
if _gateway_config is None:
|
||||||
cors_origins_str = os.getenv("CORS_ORIGINS", "http://localhost:3000")
|
|
||||||
_gateway_config = GatewayConfig(
|
_gateway_config = GatewayConfig(
|
||||||
host=os.getenv("GATEWAY_HOST", "0.0.0.0"),
|
host=os.getenv("GATEWAY_HOST", "0.0.0.0"),
|
||||||
port=int(os.getenv("GATEWAY_PORT", "8001")),
|
port=int(os.getenv("GATEWAY_PORT", "8001")),
|
||||||
cors_origins=cors_origins_str.split(","),
|
|
||||||
enable_docs=os.getenv("GATEWAY_ENABLE_DOCS", "true").lower() == "true",
|
enable_docs=os.getenv("GATEWAY_ENABLE_DOCS", "true").lower() == "true",
|
||||||
)
|
)
|
||||||
return _gateway_config
|
return _gateway_config
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ State-changing operations require CSRF protection.
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
from collections.abc import Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
from fastapi import Request, Response
|
from fastapi import Request, Response
|
||||||
@@ -106,6 +106,11 @@ def _configured_cors_origins() -> set[str]:
|
|||||||
return origins
|
return origins
|
||||||
|
|
||||||
|
|
||||||
|
def get_configured_cors_origins() -> set[str]:
|
||||||
|
"""Return normalized explicit browser origins from GATEWAY_CORS_ORIGINS."""
|
||||||
|
return _configured_cors_origins()
|
||||||
|
|
||||||
|
|
||||||
def _first_header_value(value: str | None) -> str | None:
|
def _first_header_value(value: str | None) -> str | None:
|
||||||
"""Return the first value from a comma-separated proxy header."""
|
"""Return the first value from a comma-separated proxy header."""
|
||||||
if not value:
|
if not value:
|
||||||
@@ -172,7 +177,7 @@ class CSRFMiddleware(BaseHTTPMiddleware):
|
|||||||
def __init__(self, app: ASGIApp) -> None:
|
def __init__(self, app: ASGIApp) -> None:
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
async def dispatch(self, request: Request, call_next: Callable[[Request], Awaitable[Response]]) -> Response:
|
||||||
_is_auth = is_auth_endpoint(request)
|
_is_auth = is_auth_endpoint(request)
|
||||||
|
|
||||||
if should_check_csrf(request) and _is_auth and not is_allowed_auth_origin(request):
|
if should_check_csrf(request) and _is_auth and not is_allowed_auth_origin(request):
|
||||||
|
|||||||
+12
-18
@@ -6,16 +6,16 @@ This document provides a complete reference for the DeerFlow backend APIs.
|
|||||||
|
|
||||||
DeerFlow backend exposes two sets of APIs:
|
DeerFlow backend exposes two sets of APIs:
|
||||||
|
|
||||||
1. **LangGraph API** - Agent interactions, threads, and streaming (`/api/langgraph/*`)
|
1. **LangGraph-compatible API** - Agent interactions, threads, and streaming (`/api/langgraph/*`)
|
||||||
2. **Gateway API** - Models, MCP, skills, uploads, and artifacts (`/api/*`)
|
2. **Gateway API** - Models, MCP, skills, uploads, and artifacts (`/api/*`)
|
||||||
|
|
||||||
All APIs are accessed through the Nginx reverse proxy at port 2026.
|
All APIs are accessed through the Nginx reverse proxy at port 2026.
|
||||||
|
|
||||||
## LangGraph API
|
## LangGraph-compatible API
|
||||||
|
|
||||||
Base URL: `/api/langgraph`
|
Base URL: `/api/langgraph`
|
||||||
|
|
||||||
The LangGraph API is provided by the LangGraph server and follows the LangGraph SDK conventions.
|
The public LangGraph-compatible API follows LangGraph SDK conventions. In the unified nginx deployment, Gateway owns `/api/langgraph/*` and translates those paths to its native `/api/*` run, thread, and streaming routers.
|
||||||
|
|
||||||
### Threads
|
### Threads
|
||||||
|
|
||||||
@@ -104,17 +104,11 @@ Content-Type: application/json
|
|||||||
**Recursion Limit:**
|
**Recursion Limit:**
|
||||||
|
|
||||||
`config.recursion_limit` caps the number of graph steps LangGraph will execute
|
`config.recursion_limit` caps the number of graph steps LangGraph will execute
|
||||||
in a single run. The `/api/langgraph/*` endpoints go straight to the LangGraph
|
in a single run. The unified Gateway path defaults to `100` in
|
||||||
server and therefore inherit LangGraph's native default of **25**, which is
|
`build_run_config` (see `backend/app/gateway/services.py`), which is a safer
|
||||||
too low for plan-mode or subagent-heavy runs — the agent typically errors out
|
starting point for plan-mode or subagent-heavy runs. Clients can still set
|
||||||
with `GraphRecursionError` after the first round of subagent results comes
|
`recursion_limit` explicitly in the request body; increase it if you run deeply
|
||||||
back, before the lead agent can synthesize the final answer.
|
nested subagent graphs.
|
||||||
|
|
||||||
DeerFlow's own Gateway and IM-channel paths mitigate this by defaulting to
|
|
||||||
`100` in `build_run_config` (see `backend/app/gateway/services.py`), but
|
|
||||||
clients calling the LangGraph API directly must set `recursion_limit`
|
|
||||||
explicitly in the request body. `100` matches the Gateway default and is a
|
|
||||||
safe starting point; increase it if you run deeply nested subagent graphs.
|
|
||||||
|
|
||||||
**Configurable Options:**
|
**Configurable Options:**
|
||||||
- `model_name` (string): Override the default model
|
- `model_name` (string): Override the default model
|
||||||
@@ -649,7 +643,7 @@ curl -X POST http://localhost:2026/api/langgraph/threads/abc123/runs \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
> The `/api/langgraph/*` endpoints bypass DeerFlow's Gateway and inherit
|
> The unified Gateway path defaults `config.recursion_limit` to 100 for
|
||||||
> LangGraph's native `recursion_limit` default of 25, which is too low for
|
> plan-mode and subagent-heavy runs. Clients may still set
|
||||||
> plan-mode or subagent runs. Set `config.recursion_limit` explicitly — see
|
> `config.recursion_limit` explicitly — see the [Create Run](#create-run)
|
||||||
> the [Create Run](#create-run) section for details.
|
> section for details.
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ This document provides a comprehensive overview of the DeerFlow backend architec
|
|||||||
│ Nginx (Port 2026) │
|
│ Nginx (Port 2026) │
|
||||||
│ Unified Reverse Proxy Entry Point │
|
│ Unified Reverse Proxy Entry Point │
|
||||||
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
│ ┌────────────────────────────────────────────────────────────────────┐ │
|
||||||
│ │ /api/langgraph/* → LangGraph Server (2024) │ │
|
│ │ /api/langgraph/* → Gateway LangGraph-compatible runtime (8001) │ │
|
||||||
│ │ /api/* → Gateway API (8001) │ │
|
│ │ /api/* → Gateway REST APIs (8001) │ │
|
||||||
│ │ /* → Frontend (3000) │ │
|
│ │ /* → Frontend (3000) │ │
|
||||||
│ └────────────────────────────────────────────────────────────────────┘ │
|
│ └────────────────────────────────────────────────────────────────────┘ │
|
||||||
└─────────────────────────────────┬────────────────────────────────────────┘
|
└─────────────────────────────────┬────────────────────────────────────────┘
|
||||||
@@ -24,8 +24,8 @@ This document provides a comprehensive overview of the DeerFlow backend architec
|
|||||||
│ │ │
|
│ │ │
|
||||||
▼ ▼ ▼
|
▼ ▼ ▼
|
||||||
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
||||||
│ LangGraph Server │ │ Gateway API │ │ Frontend │
|
│ Embedded Runtime │ │ Gateway API │ │ Frontend │
|
||||||
│ (Port 2024) │ │ (Port 8001) │ │ (Port 3000) │
|
│ (inside Gateway) │ │ (Port 8001) │ │ (Port 3000) │
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │
|
||||||
│ - Agent Runtime │ │ - Models API │ │ - Next.js App │
|
│ - Agent Runtime │ │ - Models API │ │ - Next.js App │
|
||||||
│ - Thread Mgmt │ │ - MCP Config │ │ - React UI │
|
│ - Thread Mgmt │ │ - MCP Config │ │ - React UI │
|
||||||
@@ -52,9 +52,9 @@ This document provides a comprehensive overview of the DeerFlow backend architec
|
|||||||
|
|
||||||
## Component Details
|
## Component Details
|
||||||
|
|
||||||
### LangGraph Server
|
### Embedded LangGraph Runtime
|
||||||
|
|
||||||
The LangGraph server is the core agent runtime, built on LangGraph for robust multi-agent workflow orchestration.
|
The LangGraph-compatible runtime runs inside the Gateway process and is built on LangGraph for robust multi-agent workflow orchestration.
|
||||||
|
|
||||||
**Entry Point**: `packages/harness/deerflow/agents/lead_agent/agent.py:make_lead_agent`
|
**Entry Point**: `packages/harness/deerflow/agents/lead_agent/agent.py:make_lead_agent`
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ The LangGraph server is the core agent runtime, built on LangGraph for robust mu
|
|||||||
|
|
||||||
### Gateway API
|
### Gateway API
|
||||||
|
|
||||||
FastAPI application providing REST endpoints for non-agent operations.
|
FastAPI application providing REST endpoints plus the public LangGraph-compatible `/api/langgraph/*` runtime routes.
|
||||||
|
|
||||||
**Entry Point**: `app/gateway/app.py`
|
**Entry Point**: `app/gateway/app.py`
|
||||||
|
|
||||||
@@ -353,10 +353,10 @@ SKILL.md Format:
|
|||||||
POST /api/langgraph/threads/{thread_id}/runs
|
POST /api/langgraph/threads/{thread_id}/runs
|
||||||
{"input": {"messages": [{"role": "user", "content": "Hello"}]}}
|
{"input": {"messages": [{"role": "user", "content": "Hello"}]}}
|
||||||
|
|
||||||
2. Nginx → LangGraph Server (2024)
|
2. Nginx → Gateway API (8001)
|
||||||
Proxied to LangGraph server
|
Routes `/api/langgraph/*` to the Gateway's LangGraph-compatible runtime
|
||||||
|
|
||||||
3. LangGraph Server
|
3. Embedded LangGraph runtime
|
||||||
a. Load/create thread state
|
a. Load/create thread state
|
||||||
b. Execute middleware chain:
|
b. Execute middleware chain:
|
||||||
- ThreadDataMiddleware: Set up paths
|
- ThreadDataMiddleware: Set up paths
|
||||||
|
|||||||
@@ -122,3 +122,45 @@ def test_health_still_works_when_docs_disabled():
|
|||||||
resp = client.get("/health")
|
resp = client.get("/health")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
assert resp.json()["status"] == "healthy"
|
assert resp.json()["status"] == "healthy"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Runtime CORS behavior
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _make_gateway_client(cors_origins: str) -> TestClient:
|
||||||
|
with patch.dict(os.environ, {"GATEWAY_CORS_ORIGINS": cors_origins}):
|
||||||
|
_reset_gateway_config()
|
||||||
|
from app.gateway.app import create_app
|
||||||
|
|
||||||
|
return TestClient(create_app())
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_cors_allows_configured_origin():
|
||||||
|
"""GATEWAY_CORS_ORIGINS should control actual browser CORS responses."""
|
||||||
|
client = _make_gateway_client("https://app.example")
|
||||||
|
|
||||||
|
response = client.get("/health", headers={"Origin": "https://app.example"})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers["access-control-allow-origin"] == "https://app.example"
|
||||||
|
assert response.headers["access-control-allow-credentials"] == "true"
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_cors_rejects_unconfigured_origin():
|
||||||
|
client = _make_gateway_client("https://app.example")
|
||||||
|
|
||||||
|
response = client.get("/health", headers={"Origin": "https://evil.example"})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert "access-control-allow-origin" not in response.headers
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_cors_normalizes_configured_default_port():
|
||||||
|
client = _make_gateway_client("https://app.example:443")
|
||||||
|
|
||||||
|
response = client.get("/health", headers={"Origin": "https://app.example"})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.headers["access-control-allow-origin"] == "https://app.example"
|
||||||
|
|||||||
@@ -53,6 +53,29 @@ def test_nginx_routes_official_langgraph_prefix_to_gateway_api():
|
|||||||
assert "proxy_pass http://gateway" in content or "proxy_pass http://$gateway_upstream" in content
|
assert "proxy_pass http://gateway" in content or "proxy_pass http://$gateway_upstream" in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_nginx_defers_cors_to_gateway_allowlist():
|
||||||
|
for path in ("docker/nginx/nginx.local.conf", "docker/nginx/nginx.conf"):
|
||||||
|
content = _read(path)
|
||||||
|
|
||||||
|
assert "Access-Control-Allow-Origin" not in content
|
||||||
|
assert "Access-Control-Allow-Methods" not in content
|
||||||
|
assert "Access-Control-Allow-Headers" not in content
|
||||||
|
assert "Access-Control-Allow-Credentials" not in content
|
||||||
|
assert "proxy_hide_header 'Access-Control-Allow-" not in content
|
||||||
|
assert "if ($request_method = 'OPTIONS')" not in content
|
||||||
|
|
||||||
|
|
||||||
|
def test_gateway_cors_configuration_uses_gateway_allowlist():
|
||||||
|
gateway_config = _read("backend/app/gateway/config.py")
|
||||||
|
gateway_app = _read("backend/app/gateway/app.py")
|
||||||
|
csrf_middleware = _read("backend/app/gateway/csrf_middleware.py")
|
||||||
|
|
||||||
|
assert not re.search(r"(?<!GATEWAY_)[\"']CORS_ORIGINS[\"']", gateway_config)
|
||||||
|
assert "cors_origins" not in gateway_config
|
||||||
|
assert "get_configured_cors_origins" in gateway_app
|
||||||
|
assert "GATEWAY_CORS_ORIGINS" in csrf_middleware
|
||||||
|
|
||||||
|
|
||||||
def test_frontend_rewrites_langgraph_prefix_to_gateway():
|
def test_frontend_rewrites_langgraph_prefix_to_gateway():
|
||||||
next_config = _read("frontend/next.config.js")
|
next_config = _read("frontend/next.config.js")
|
||||||
api_client = _read("frontend/src/core/api/api-client.ts")
|
api_client = _read("frontend/src/core/api/api-client.ts")
|
||||||
|
|||||||
+5
-15
@@ -28,21 +28,11 @@ http {
|
|||||||
set $gateway_upstream gateway:8001;
|
set $gateway_upstream gateway:8001;
|
||||||
set $frontend_upstream frontend:3000;
|
set $frontend_upstream frontend:3000;
|
||||||
|
|
||||||
# Hide CORS headers from upstream to prevent duplicates
|
# Keep the unified nginx endpoint same-origin by default. When split
|
||||||
proxy_hide_header 'Access-Control-Allow-Origin';
|
# frontend/backend or port-forwarded deployments need browser CORS,
|
||||||
proxy_hide_header 'Access-Control-Allow-Methods';
|
# configure the Gateway allowlist with GATEWAY_CORS_ORIGINS so CORS and
|
||||||
proxy_hide_header 'Access-Control-Allow-Headers';
|
# CSRF origin checks stay aligned instead of approving every origin at
|
||||||
proxy_hide_header 'Access-Control-Allow-Credentials';
|
# the proxy layer.
|
||||||
|
|
||||||
# CORS headers for all responses (nginx handles CORS centrally)
|
|
||||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
|
|
||||||
add_header 'Access-Control-Allow-Headers' '*' always;
|
|
||||||
|
|
||||||
# Handle OPTIONS requests (CORS preflight)
|
|
||||||
if ($request_method = 'OPTIONS') {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
# LangGraph-compatible API routes served by Gateway.
|
# LangGraph-compatible API routes served by Gateway.
|
||||||
# Rewrites /api/langgraph/* to /api/* before proxying to Gateway.
|
# Rewrites /api/langgraph/* to /api/* before proxying to Gateway.
|
||||||
|
|||||||
@@ -28,21 +28,11 @@ http {
|
|||||||
listen [::]:2026;
|
listen [::]:2026;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
# Hide CORS headers from upstream to prevent duplicates
|
# Keep the unified nginx endpoint same-origin by default. When split
|
||||||
proxy_hide_header 'Access-Control-Allow-Origin';
|
# frontend/backend or port-forwarded deployments need browser CORS,
|
||||||
proxy_hide_header 'Access-Control-Allow-Methods';
|
# configure the Gateway allowlist with GATEWAY_CORS_ORIGINS so CORS and
|
||||||
proxy_hide_header 'Access-Control-Allow-Headers';
|
# CSRF origin checks stay aligned instead of approving every origin at
|
||||||
proxy_hide_header 'Access-Control-Allow-Credentials';
|
# the proxy layer.
|
||||||
|
|
||||||
# CORS headers for all responses (nginx handles CORS centrally)
|
|
||||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
|
|
||||||
add_header 'Access-Control-Allow-Headers' '*' always;
|
|
||||||
|
|
||||||
# Handle OPTIONS requests (CORS preflight)
|
|
||||||
if ($request_method = 'OPTIONS') {
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
|
|
||||||
# LangGraph-compatible API routes served by Gateway.
|
# LangGraph-compatible API routes served by Gateway.
|
||||||
# Rewrites /api/langgraph/* to /api/* before proxying to Gateway.
|
# Rewrites /api/langgraph/* to /api/* before proxying to Gateway.
|
||||||
|
|||||||
Reference in New Issue
Block a user