mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-24 07:42:21 +02:00
merge: reconcile PR 40 with current lab
Integrate lab fff55a78 into PR #40 (cc25d5ba). Lab's modular email backend/frontend, modular settings, split stylesheets (static/style.css stays deleted), procfs compatibility, and request-scoped TurnContract authority win; PR #40's routing classifiers, editor/email/task features, and style.css changes are ported into lab's module and stylesheet homes. Integration fixes: - settings/api.js imports ui.js under its canonical versioned URL - browser observations keep legacy CAPTCHA/access-block evidence - artifact turns do not re-trigger broad-web research recovery - env reference documents PR test-tool variables; page regenerated PR #40 defects surfaced by lab gates and fixed here: - web_fetch generic schema drops top-level anyOf (OpenAI contract); the compact preview contract still requires url or urls - get_weather registered as a brokered network read - new lazy editor modules precached for offline use - SearXNG pin mirrored into GPU standalone compose files - image model picker again skips offline endpoints Tests updated where PR #40 changed behaviour on purpose, and PR tests moved onto lab's document_source helpers.
This commit is contained in:
+10
-4
@@ -3524,11 +3524,17 @@ def setup_chat_routes(
|
||||
# has an exact routed contract. Adding the whole native
|
||||
# workspace inventory here reintroduced overlapping PDF
|
||||
# readers and caused the model to abandon the selected
|
||||
# OCR operation. Exact operations therefore stay exact;
|
||||
# ordinary native turns retain warm and workspace tools.
|
||||
# OCR operation. A task-only turn likewise has an exact
|
||||
# personal manager; core shell/Web tools are not task
|
||||
# fallbacks. Other turns retain warm and workspace tools.
|
||||
extra_tools=(
|
||||
INTERACTIVE_CORE_TOOLS
|
||||
if _exact_selected_native_chain
|
||||
frozenset()
|
||||
if _exact_selected_native_chain or _active_turn_capabilities == frozenset({"tasks"}) or (
|
||||
_active_turn_capabilities in (
|
||||
frozenset({"transcription"}), frozenset({"ocr"}),
|
||||
)
|
||||
and not _selected_tools
|
||||
)
|
||||
else INTERACTIVE_CORE_TOOLS | _warm_tools | (
|
||||
NATIVE_WORKSPACE_TOOLS | (
|
||||
{"private_browser"} if _local_browser_render_intent else frozenset()
|
||||
|
||||
@@ -19,9 +19,10 @@ Each draft carries:
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request
|
||||
from fastapi import APIRouter, HTTPException, Request, Response
|
||||
from fastapi.routing import APIRoute
|
||||
from pydantic import BaseModel
|
||||
|
||||
from core.database import EditorDraft, SessionLocal
|
||||
@@ -76,18 +77,56 @@ def _load_payload(raw: Optional[str]) -> Dict[str, Any]:
|
||||
return payload if isinstance(payload, dict) else {}
|
||||
|
||||
|
||||
def _draft_too_large() -> HTTPException:
|
||||
return HTTPException(
|
||||
413,
|
||||
f"Editor draft exceeds the {EDITOR_DRAFT_MAX_BYTES // (1024 * 1024)} MB safety limit",
|
||||
)
|
||||
|
||||
|
||||
def reject_oversized_draft_body(request: Request) -> None:
|
||||
"""Refuse an oversized draft on declared ``Content-Length``, before the body is read or parsed.
|
||||
|
||||
``_dump_payload`` still owns the authoritative byte count, but it only runs
|
||||
after the request has been parsed and re-serialised. Declaring a body past the
|
||||
ceiling is enough to reject it early and cheaply. Requests with absent,
|
||||
malformed, or chunked transfer encoding still hit the authoritative byte count
|
||||
check further down.
|
||||
"""
|
||||
raw_length = request.headers.get("content-length")
|
||||
if not raw_length:
|
||||
return
|
||||
try:
|
||||
declared = int(raw_length)
|
||||
except (TypeError, ValueError):
|
||||
return
|
||||
if declared > EDITOR_DRAFT_MAX_BYTES:
|
||||
raise _draft_too_large()
|
||||
|
||||
|
||||
class EditorDraftRoute(APIRoute):
|
||||
"""Route class that validates declared Content-Length before request body parsing."""
|
||||
|
||||
def get_route_handler(self) -> Callable:
|
||||
original_route_handler = super().get_route_handler()
|
||||
|
||||
async def custom_route_handler(request: Request) -> Response:
|
||||
if request.method in ("POST", "PUT", "PATCH"):
|
||||
reject_oversized_draft_body(request)
|
||||
return await original_route_handler(request)
|
||||
|
||||
return custom_route_handler
|
||||
|
||||
|
||||
def _dump_payload(payload: Dict[str, Any]) -> str:
|
||||
raw = json.dumps(payload or {}, separators=(",", ":"))
|
||||
if len(raw.encode("utf-8")) > EDITOR_DRAFT_MAX_BYTES:
|
||||
raise HTTPException(
|
||||
413,
|
||||
f"Editor draft exceeds the {EDITOR_DRAFT_MAX_BYTES // (1024 * 1024)} MB safety limit",
|
||||
)
|
||||
raise _draft_too_large()
|
||||
return raw
|
||||
|
||||
|
||||
def setup_editor_draft_routes() -> APIRouter:
|
||||
router = APIRouter(tags=["editor-drafts"])
|
||||
router = APIRouter(tags=["editor-drafts"], route_class=EditorDraftRoute)
|
||||
|
||||
@router.get("/api/editor-drafts")
|
||||
async def list_drafts(request: Request) -> Dict[str, List[Dict[str, Any]]]:
|
||||
@@ -120,7 +159,10 @@ def setup_editor_draft_routes() -> APIRouter:
|
||||
db.close()
|
||||
|
||||
@router.post("/api/editor-drafts")
|
||||
async def create_draft(request: Request, body: DraftCreate) -> Dict[str, Any]:
|
||||
async def create_draft(
|
||||
request: Request,
|
||||
body: DraftCreate,
|
||||
) -> Dict[str, Any]:
|
||||
user = get_current_user(request)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@@ -148,7 +190,11 @@ def setup_editor_draft_routes() -> APIRouter:
|
||||
db.close()
|
||||
|
||||
@router.put("/api/editor-drafts/{draft_id}")
|
||||
async def update_draft(request: Request, draft_id: str, body: DraftUpdate) -> Dict[str, Any]:
|
||||
async def update_draft(
|
||||
request: Request,
|
||||
draft_id: str,
|
||||
body: DraftUpdate,
|
||||
) -> Dict[str, Any]:
|
||||
user = get_current_user(request)
|
||||
db = SessionLocal()
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Email route domain package.
|
||||
|
||||
Contains email_routes.py, email_helpers.py and email_pollers.py, migrated
|
||||
from the flat routes/ directory. Backward-compat shims at
|
||||
routes/email_routes.py, routes/email_helpers.py and routes/email_pollers.py
|
||||
replace themselves with these modules, so both import paths resolve to one
|
||||
object.
|
||||
"""
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+11
-2019
File diff suppressed because it is too large
Load Diff
+11
-1759
File diff suppressed because it is too large
Load Diff
+11
-7226
File diff suppressed because it is too large
Load Diff
@@ -181,10 +181,17 @@ def setup_mcp_routes(mcp_manager: McpManager):
|
||||
if transport == "http" and not url:
|
||||
raise HTTPException(400, "url is required for HTTP transport")
|
||||
|
||||
# Parse JSON fields
|
||||
try:
|
||||
parsed_args = json.loads(args) if args else []
|
||||
except json.JSONDecodeError:
|
||||
# Parse JSON fields. args is not defaulted on a parse failure: an
|
||||
# unparseable value is silently discarded downstream (stdio spawns
|
||||
# with an empty argv), so the caller must be told instead.
|
||||
if args:
|
||||
try:
|
||||
parsed_args = json.loads(args)
|
||||
except json.JSONDecodeError:
|
||||
raise HTTPException(400, "args must be valid JSON, e.g. [\"-y\", \"pkg\"]")
|
||||
if not isinstance(parsed_args, list):
|
||||
raise HTTPException(400, "args must be a JSON array, e.g. [\"-y\", \"pkg\"]")
|
||||
else:
|
||||
parsed_args = []
|
||||
try:
|
||||
parsed_env = json.loads(env) if env else {}
|
||||
|
||||
Reference in New Issue
Block a user