mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-28 17:06:05 +00:00
* feat(browser): add agentic browser control
* fix(frontend): format browser view changes
* fix(browser): keep browser optional and isolate sidecar layout
* fix(browser): address PR review security and IME findings
- Nginx: add a browser-stream WebSocket location before the generic
/api/threads regex so Live upgrades instead of downgrading to HTTP
(both nginx.conf and nginx.local.conf).
- Ownership: require an existing owned thread for the WS stream and REST
navigate, and tear down the browser session on thread deletion so a
later caller cannot reuse a retained page/cookies by guessing the id.
- SSRF: enforce the URL policy at the browser request boundary via a
context-level route guard covering redirects, popups, iframes, and
subresources (skipped for CDP-attached Chrome).
- IME: skip key forwarding while a composition is active so confirming a
CJK candidate with Enter no longer submits the remote page form.
Adds regression tests for the request guard, session teardown on delete,
and the composing-Enter key decision.
* fix(frontend): smooth streaming in long tool threads
* Revert "fix(frontend): smooth streaming in long tool threads"
This reverts commit f0462516eabe77f138d4027ea1c714fb226683cf.
* fix(browser): address review security and lifecycle findings
- Reject cross-origin WebSocket upgrades on the live browser stream
(Origin allow-list reuse of CORS/same-origin helpers) to close a
WS-CSRF hole, and fail closed when the ownership store is absent.
- Warn when a CDP-attached session runs with the SSRF request guard
off, and drop the unreachable CDP screencast teardown dead code.
- Read browser session launch config from a single canonical source
(browser_navigate) so it is deterministic regardless of call order.
- Bound per-thread Chromium accumulation with idle-timeout eviction
and an LRU max-sessions cap.
- Reset the Live reconnect counter on a successful open so the stream
can't permanently stall after the cumulative attempt cap.
* fix(frontend): reduce long tool thread render stalls
Reuse stable historical message groups during streaming, defer heavy Markdown and browser previews, and lazy-decode message images.
* fix(browser): keep live control responsive during continuous input
Why: Manual browser control felt laggy — a physical click ran the remote
Playwright click three times and each non-move input synchronously awaited a
JPEG screenshot, so events queued behind capture (queue wait up to ~237ms).
The first async attempt used a trailing-edge debounce, which froze the visible
page until a wheel/keyboard gesture stopped ("scroll finishes, then it jumps").
What:
- Frontend forwards one `click` per physical click instead of also emitting
`down`/`up`, so the remote page is not clicked twice per gesture.
- Backend detaches live-frame capture from input dispatch: non-move actions
start a rate-limited background refresh loop (leading frame + bounded cadence)
that keeps emitting frames while input continues and never blocks dispatch.
- Add regression tests: input dispatch no longer awaits the screenshot, rapid
inputs coalesce, and continuous input keeps refreshing before it stops.
Scenarios: Verified in the live Browser panel — a single click completes in
~57ms (was blocked behind a 171ms capture), and a 1.14s sustained wheel gesture
renders ~7 frames throughout the scroll instead of one frame after it ends.
* fix(browser): harden worker and session lifecycle
* fix(browser): address latest review feedback
* fix(frontend): preserve optimistic new-chat message
* test(e2e): preserve mocked message run ids
* fix(browser): address capability review feedback
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
323 lines
12 KiB
Plaintext
323 lines
12 KiB
Plaintext
# Global error_log (main context). Without this, nginx opens its compiled-in
|
|
# default (/var/log/nginx/error.log) at startup before reaching the http-block
|
|
# error_log, which fails with permission denied when run as a non-root user.
|
|
error_log logs/nginx-error.log warn;
|
|
events {
|
|
worker_connections 1024;
|
|
}
|
|
pid logs/nginx.pid;
|
|
http {
|
|
# Basic settings
|
|
sendfile on;
|
|
tcp_nopush on;
|
|
tcp_nodelay on;
|
|
keepalive_timeout 65;
|
|
types_hash_max_size 2048;
|
|
|
|
# Logging
|
|
access_log logs/nginx-access.log;
|
|
error_log logs/nginx-error.log;
|
|
|
|
# Upstream servers (using 127.0.0.1 for local development)
|
|
upstream gateway {
|
|
server 127.0.0.1:8001;
|
|
}
|
|
|
|
upstream frontend {
|
|
server 127.0.0.1:3000;
|
|
}
|
|
|
|
# Only mark frontend requests as connection upgrades when the browser
|
|
# actually requested one. Next.js dev HMR also uses long-lived HTTP
|
|
# streams, and forcing "Connection: upgrade" on those requests makes
|
|
# nginx treat the upstream response as an invalid upgrade handshake.
|
|
map $http_upgrade $connection_upgrade {
|
|
default upgrade;
|
|
'' '';
|
|
}
|
|
|
|
server {
|
|
listen 2026;
|
|
listen [::]:2026;
|
|
server_name _;
|
|
|
|
# Keep the unified nginx endpoint same-origin by default. When split
|
|
# frontend/backend or port-forwarded deployments need browser CORS,
|
|
# configure the Gateway allowlist with GATEWAY_CORS_ORIGINS so CORS and
|
|
# CSRF origin checks stay aligned instead of approving every origin at
|
|
# the proxy layer.
|
|
|
|
# LangGraph-compatible API routes served by Gateway.
|
|
# Rewrites /api/langgraph/* to /api/* before proxying to Gateway.
|
|
location /api/langgraph/ {
|
|
rewrite ^/api/langgraph/(.*) /api/$1 break;
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
|
|
# Headers
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header Connection '';
|
|
|
|
# SSE/Streaming support
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_set_header X-Accel-Buffering no;
|
|
|
|
# Long chat/text-prompt support (issue #3952): a sufficiently long
|
|
# pasted prompt otherwise exceeds nginx's default 1m
|
|
# client_max_body_size, or gets spooled to a temp file via
|
|
# proxy_request_buffering before reaching Gateway -- on a non-root
|
|
# local run that temp directory may not be writable, producing a
|
|
# raw nginx 500 instead of a graceful application error. Mirrors
|
|
# the uploads location's fix below (same mechanism, sized for text
|
|
# prompts rather than binary file uploads).
|
|
client_max_body_size 20M;
|
|
proxy_request_buffering off;
|
|
|
|
# Timeouts for long-running requests
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
|
|
# Chunked transfer encoding
|
|
chunked_transfer_encoding on;
|
|
}
|
|
|
|
# Custom API: Models endpoint
|
|
location /api/models {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
# Disable buffering to avoid permission errors when nginx
|
|
# runs as a non-root user (e.g. local development).
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: Memory endpoint
|
|
location /api/memory {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: MCP configuration endpoint
|
|
location /api/mcp {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: Skills configuration endpoint
|
|
location /api/skills {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: Agents endpoint
|
|
location /api/agents {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Custom API: Uploads endpoint
|
|
location ~ ^/api/threads/[^/]+/uploads {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
# Large file upload support
|
|
client_max_body_size 100M;
|
|
proxy_request_buffering off;
|
|
|
|
# Disable response buffering to avoid permission errors
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Live browser stream is a WebSocket upgrade. It must be matched before
|
|
# the generic /api/threads regex below (which omits Upgrade/Connection
|
|
# forwarding and would downgrade it to plain HTTP).
|
|
location ~ ^/api/threads/[^/]+/browser/stream {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection 'upgrade';
|
|
proxy_cache_bypass $http_upgrade;
|
|
|
|
# Long-lived Live stream: keep the upgraded connection open.
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
}
|
|
|
|
# Custom API: Other endpoints under /api/threads
|
|
location ~ ^/api/threads {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# API Documentation: Swagger UI
|
|
location /api/docs {
|
|
proxy_pass http://gateway/docs ;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# API Documentation: ReDoc
|
|
location /api/redoc {
|
|
proxy_pass http://gateway/redoc;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# API Documentation: OpenAPI Schema
|
|
location /openapi.json {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Health check endpoint (gateway)
|
|
location /health {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# Catch-all for any /api/* prefix not matched by a more specific block above.
|
|
# Covers the auth module (/api/v1/auth/login, /me, /change-password, ...),
|
|
# plus feedback / runs / token-usage routes that 2.0-rc added without
|
|
# updating this nginx config. Longest-prefix matching ensures the explicit
|
|
# blocks above (/api/models, /api/threads regex, /api/langgraph/, ...) still
|
|
# win for their paths — only truly unmatched /api/* requests land here.
|
|
location /api/ {
|
|
proxy_pass http://gateway;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
# Auth endpoints set HttpOnly cookies — make sure nginx doesn't
|
|
# strip the Set-Cookie header from upstream responses.
|
|
proxy_pass_header Set-Cookie;
|
|
|
|
# Disable buffering to avoid permission errors when nginx
|
|
# runs as a non-root user (e.g. local development).
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
}
|
|
|
|
# All other requests go to frontend
|
|
location / {
|
|
proxy_pass http://frontend;
|
|
proxy_http_version 1.1;
|
|
|
|
# Headers
|
|
proxy_set_header Host $http_host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_cache_bypass $http_upgrade;
|
|
|
|
# Disable response buffering for the frontend. Without this,
|
|
# nginx tries to spool large upstream responses (e.g. Next.js
|
|
# static chunks) into ``proxy_temp_path``, which defaults to
|
|
# the system-owned ``/var/lib/nginx/proxy`` and fails with
|
|
# ``[crit] open() ... failed (13: Permission denied)`` when
|
|
# nginx is launched as a non-root user (every dev machine
|
|
# except production root containers). The symptom on the
|
|
# client side is ``ERR_INCOMPLETE_CHUNKED_ENCODING`` and
|
|
# ``ChunkLoadError`` partway through page hydration.
|
|
#
|
|
# Streaming the response straight through avoids the
|
|
# temp-file path entirely. The frontend already sets its
|
|
# own cache headers, so we don't lose anything from
|
|
# disabling nginx-side buffering.
|
|
proxy_buffering off;
|
|
proxy_request_buffering off;
|
|
|
|
# Timeouts
|
|
proxy_connect_timeout 600s;
|
|
proxy_send_timeout 600s;
|
|
proxy_read_timeout 600s;
|
|
}
|
|
}
|
|
}
|