Vendored deer-flow upstream (bytedance/deer-flow) plus prompt-injection hardening: - New deerflow.security package: content_delimiter, html_cleaner, sanitizer (8 layers — invisible chars, control chars, symbols, NFC, PUA, tag chars, horizontal whitespace collapse with newline/tab preservation, length cap) - New deerflow.community.searx package: web_search, web_fetch, image_search backed by a private SearX instance, every external string sanitized and wrapped in <<<EXTERNAL_UNTRUSTED_CONTENT>>> delimiters - All native community web providers (ddg_search, tavily, exa, firecrawl, jina_ai, infoquest, image_search) replaced with hard-fail stubs that raise NativeWebToolDisabledError at import time, so a misconfigured tool.use path fails loud rather than silently falling back to unsanitized output - Native client back-doors (jina_client.py, infoquest_client.py) stubbed too - Native-tool tests quarantined under tests/_disabled_native/ (collect_ignore_glob via local conftest.py) - Sanitizer Layer 7 fix: only collapse horizontal whitespace, preserve newlines and tabs so list/table structure survives - Hardened runtime config.yaml references only the searx-backed tools - Factory overlay (backend/) kept in sync with deer-flow tree as a reference / source See HARDENING.md for the full audit trail and verification steps.
71 lines
2.4 KiB
Python
71 lines
2.4 KiB
Python
"""Centralized accessors for singleton objects stored on ``app.state``.
|
||
|
||
**Getters** (used by routers): raise 503 when a required dependency is
|
||
missing, except ``get_store`` which returns ``None``.
|
||
|
||
Initialization is handled directly in ``app.py`` via :class:`AsyncExitStack`.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from collections.abc import AsyncGenerator
|
||
from contextlib import AsyncExitStack, asynccontextmanager
|
||
|
||
from fastapi import FastAPI, HTTPException, Request
|
||
|
||
from deerflow.runtime import RunManager, StreamBridge
|
||
|
||
|
||
@asynccontextmanager
|
||
async def langgraph_runtime(app: FastAPI) -> AsyncGenerator[None, None]:
|
||
"""Bootstrap and tear down all LangGraph runtime singletons.
|
||
|
||
Usage in ``app.py``::
|
||
|
||
async with langgraph_runtime(app):
|
||
yield
|
||
"""
|
||
from deerflow.agents.checkpointer.async_provider import make_checkpointer
|
||
from deerflow.runtime import make_store, make_stream_bridge
|
||
|
||
async with AsyncExitStack() as stack:
|
||
app.state.stream_bridge = await stack.enter_async_context(make_stream_bridge())
|
||
app.state.checkpointer = await stack.enter_async_context(make_checkpointer())
|
||
app.state.store = await stack.enter_async_context(make_store())
|
||
app.state.run_manager = RunManager()
|
||
yield
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Getters – called by routers per-request
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def get_stream_bridge(request: Request) -> StreamBridge:
|
||
"""Return the global :class:`StreamBridge`, or 503."""
|
||
bridge = getattr(request.app.state, "stream_bridge", None)
|
||
if bridge is None:
|
||
raise HTTPException(status_code=503, detail="Stream bridge not available")
|
||
return bridge
|
||
|
||
|
||
def get_run_manager(request: Request) -> RunManager:
|
||
"""Return the global :class:`RunManager`, or 503."""
|
||
mgr = getattr(request.app.state, "run_manager", None)
|
||
if mgr is None:
|
||
raise HTTPException(status_code=503, detail="Run manager not available")
|
||
return mgr
|
||
|
||
|
||
def get_checkpointer(request: Request):
|
||
"""Return the global checkpointer, or 503."""
|
||
cp = getattr(request.app.state, "checkpointer", None)
|
||
if cp is None:
|
||
raise HTTPException(status_code=503, detail="Checkpointer not available")
|
||
return cp
|
||
|
||
|
||
def get_store(request: Request):
|
||
"""Return the global store (may be ``None`` if not configured)."""
|
||
return getattr(request.app.state, "store", None)
|