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:
AochenShen99
2026-05-11 17:38:37 +08:00
committed by GitHub
parent 813d3c94ef
commit c3bc6c7cd5
14 changed files with 169 additions and 130 deletions
+3 -2
View File
@@ -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
+12 -18
View File
@@ -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,8 +131,7 @@ 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
``` ```
@@ -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
+2
View File
@@ -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
View File
@@ -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/`)
+13 -10
View File
@@ -14,14 +14,17 @@ 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│ │ │ └────────────────────────┘ ┌────────────────┐ │
│ │ Lead Agent │ │
│ │ ┌──────────┐ │ │
│ │ │Middleware│ │ │
│ │ │ Chain │ │ │ │ │ │ Chain │ │ │
│ │ └──────────┘ │ │ │ │ └──────────┘ │ │
│ │ ┌──────────┐ │ │ │ │ ┌──────────┐ │ │
@@ -31,11 +34,11 @@ DeerFlow is a LangGraph-based AI super agent with sandbox execution, persistent
│ │ │Subagents │ │ │ │ │ │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
+14 -18
View File
@@ -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,17 +310,10 @@ 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()]
# Validate: wildcard origin with credentials is a security misconfiguration
for origin in cors_origins:
if origin == "*":
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.")
cors_origins = [o for o in cors_origins if o != "*"]
break
if cors_origins: if cors_origins:
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@@ -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:
-3
View File
@@ -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
+7 -2
View File
@@ -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
View File
@@ -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.
+10 -10
View File
@@ -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
+42
View File
@@ -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
View File
@@ -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.
+5 -15
View File
@@ -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.