mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-29 01:02:25 +01:00
Merge current dev and refresh PR #6738 from the 7011 integration
This commit is contained in:
+77
-13
@@ -6,17 +6,24 @@ import uuid
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Form, HTTPException, Response, Request, Query
|
||||
from fastapi import APIRouter, Form, HTTPException, Response, Request, Depends, Query
|
||||
import logging
|
||||
|
||||
from core.session_manager import SessionManager
|
||||
from core.models import ChatMessage
|
||||
from src.request_models import SessionResponse
|
||||
from core.database import Session as DbSession, SessionLocal, Document, GalleryImage, utcnow_naive
|
||||
from src.auth_helpers import effective_user, _auth_disabled, owner_filter
|
||||
from src.auth_helpers import (
|
||||
effective_user,
|
||||
_auth_disabled,
|
||||
owner_filter,
|
||||
is_delegated_credential,
|
||||
require_chat_api_token_scope,
|
||||
)
|
||||
from src.session_image_cleanup import _generated_image_path_for_cleanup, session_image_refs
|
||||
from src.session_actions import is_session_recently_active
|
||||
from src.upload_handler import reserve_message_upload_references
|
||||
from src.tool_approval_scopes import sanitize_client_message_metadata
|
||||
|
||||
|
||||
def _sanitize_export_filename(name: str) -> str:
|
||||
@@ -234,9 +241,15 @@ def _verify_session_owner(request: Request, session_id: str, session_manager=Non
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["sessions"])
|
||||
router = APIRouter(
|
||||
prefix="/api",
|
||||
tags=["sessions"],
|
||||
dependencies=[Depends(require_chat_api_token_scope)],
|
||||
)
|
||||
|
||||
def _current_user_is_admin(request: Request, user: str | None) -> bool:
|
||||
if is_delegated_credential(request):
|
||||
return False
|
||||
if not user:
|
||||
return False
|
||||
auth_mgr = getattr(request.app.state, "auth_manager", None)
|
||||
@@ -267,6 +280,22 @@ def _reject_raw_endpoint_url_for_non_admin(
|
||||
raise HTTPException(403, "Choose a registered model endpoint")
|
||||
|
||||
|
||||
def _reject_delegated_session_options(
|
||||
request: Request,
|
||||
*,
|
||||
skip_validation: bool = False,
|
||||
api_key: str | None = None,
|
||||
) -> None:
|
||||
"""Keep bearer credentials from exercising interactive-admin options."""
|
||||
if is_delegated_credential(request) and (
|
||||
skip_validation or bool((api_key or "").strip())
|
||||
):
|
||||
raise HTTPException(
|
||||
403,
|
||||
"API tokens cannot supply endpoint credentials or skip endpoint validation",
|
||||
)
|
||||
|
||||
|
||||
def _persist_session_headers(session_id: str, headers: dict | None) -> bool:
|
||||
"""Persist endpoint auth headers for DB-backed session metadata."""
|
||||
delays = (0.05, 0.15, 0.35)
|
||||
@@ -480,6 +509,7 @@ def setup_session_routes(
|
||||
"has_documents": s.id in doc_session_ids,
|
||||
"has_images": s.id in img_session_ids,
|
||||
"mode": s.mode,
|
||||
"thinking_mode": s.thinking_mode or "off",
|
||||
"message_count": s.message_count or 0,
|
||||
})
|
||||
finally:
|
||||
@@ -501,6 +531,11 @@ def setup_session_routes(
|
||||
):
|
||||
skip_val = str(skip_validation).lower() == "true"
|
||||
user = effective_user(request)
|
||||
_reject_delegated_session_options(
|
||||
request,
|
||||
skip_validation=skip_val,
|
||||
api_key=api_key,
|
||||
)
|
||||
endpoint_api_key = ""
|
||||
endpoint_base_url = ""
|
||||
_reject_raw_endpoint_url_for_non_admin(request, user, endpoint_id, endpoint_url)
|
||||
@@ -757,7 +792,11 @@ def setup_session_routes(
|
||||
except (AttributeError, TypeError, ValueError) as exc:
|
||||
raise HTTPException(400, "Invalid message attachment metadata") from exc
|
||||
for m in messages:
|
||||
sess.add_message(ChatMessage(m["role"], m["content"], metadata=m.get("metadata")))
|
||||
sess.add_message(ChatMessage(
|
||||
m["role"],
|
||||
m["content"],
|
||||
metadata=sanitize_client_message_metadata(m.get("metadata")),
|
||||
))
|
||||
session_manager.save_sessions()
|
||||
return {"ok": True, "count": len(messages)}
|
||||
|
||||
@@ -1004,6 +1043,7 @@ def setup_session_routes(
|
||||
"id": s.id,
|
||||
"name": s.name,
|
||||
"model": s.model,
|
||||
"thinking_mode": s.thinking_mode or "off",
|
||||
"message_count": s.message_count or 0,
|
||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
|
||||
@@ -1118,6 +1158,8 @@ def setup_session_routes(
|
||||
model: str = Form("gpt-4o"),
|
||||
rag: str = Form(None)
|
||||
):
|
||||
if is_delegated_credential(request):
|
||||
raise HTTPException(403, "This session type requires an interactive session")
|
||||
if not OPENAI_API_KEY:
|
||||
raise HTTPException(400, "Server missing OPENAI_API_KEY")
|
||||
sid = str(uuid.uuid4())
|
||||
@@ -1198,9 +1240,16 @@ def setup_session_routes(
|
||||
from src.context_compactor import SELF_SUMMARY_SYSTEM_PROMPT
|
||||
from src.endpoint_resolver import resolve_endpoint
|
||||
from src.llm_core import llm_call_async
|
||||
from src.utility_effort import effort_for_call
|
||||
|
||||
owner = getattr(session, "owner", None) or effective_user(request)
|
||||
url, model, headers = resolve_endpoint("utility", owner=owner)
|
||||
url, model, headers = resolve_endpoint(
|
||||
"utility",
|
||||
fallback_url=getattr(session, "endpoint_url", None),
|
||||
fallback_model=getattr(session, "model", None),
|
||||
fallback_headers=getattr(session, "headers", None),
|
||||
owner=owner,
|
||||
)
|
||||
if not url or not model:
|
||||
url, model, headers = session.endpoint_url, session.model, session.headers
|
||||
if not url or not model:
|
||||
@@ -1228,6 +1277,7 @@ def setup_session_routes(
|
||||
max_tokens=1024,
|
||||
headers=headers,
|
||||
timeout=60,
|
||||
reasoning_effort=effort_for_call(url, model, owner, session),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Manual compaction failed: %s", e)
|
||||
@@ -1270,7 +1320,8 @@ def setup_session_routes(
|
||||
after Phase 1 — used by the "Tidy (no AI)" UI affordance so
|
||||
users can clean junk without spending tokens.
|
||||
"""
|
||||
from src.llm_core import llm_call
|
||||
from src.llm_core import llm_call, llm_call_with_fallback
|
||||
from src.utility_effort import effort_for_call
|
||||
user = effective_user(request)
|
||||
single_user_mode = not user and _auth_disabled()
|
||||
user_sessions = session_manager.get_sessions_for_user(user)
|
||||
@@ -1416,7 +1467,7 @@ def setup_session_routes(
|
||||
return {"status": "skipped", "reason": "No unfiled sessions to sort"}
|
||||
|
||||
# Pick an endpoint — prefer admin-configured task endpoint
|
||||
from src.task_endpoint import resolve_task_endpoint
|
||||
from src.task_endpoint import resolve_task_endpoint, resolve_task_candidates
|
||||
url, model, headers = resolve_task_endpoint(owner=user)
|
||||
if not url:
|
||||
url, model, headers = _pick_endpoint_for_sort(owner=user)
|
||||
@@ -1442,8 +1493,12 @@ def setup_session_routes(
|
||||
# 16384 (was 4096): with many chats the folder JSON is large, and a
|
||||
# reasoning model spends tokens thinking first — 4096 truncated the
|
||||
# JSON mid-output, so it never parsed ("invalid JSON for auto-sort").
|
||||
raw = llm_call(url, model, [{"role": "user", "content": prompt}],
|
||||
temperature=0.3, max_tokens=16384, headers=headers, timeout=120)
|
||||
raw = llm_call_with_fallback(
|
||||
resolve_task_candidates(fallback_url=url, fallback_model=model,
|
||||
fallback_headers=headers, owner=user),
|
||||
[{"role": "user", "content": prompt}],
|
||||
temperature=0.3, max_tokens=16384, timeout=120,
|
||||
reasoning_effort=effort_for_call(url, model, user))
|
||||
logger.info(f"Auto-sort raw response ({len(raw)} chars): {raw[:300]}")
|
||||
# Extract JSON from response — handle markdown fences, leading text,
|
||||
# reasoning-model <think> blocks, and trailing commas.
|
||||
@@ -1561,7 +1616,13 @@ def setup_session_routes(
|
||||
raise HTTPException(404, "Session not found")
|
||||
skills = _context_info_skill_inventory(skills_manager, owner=owner)
|
||||
tools = _context_info_tool_inventory()
|
||||
agents_md = _context_info_agents_md_inventory(cwd)
|
||||
# `cwd` is caller-chosen. Walking it for AGENTS.md and reporting whether
|
||||
# it exists confirms paths on the host, which /api/workspace/browse and
|
||||
# /vet reserve for admins (or single-user mode); keep that boundary here.
|
||||
from src.tool_security import owner_is_admin_or_single_user
|
||||
|
||||
_host_paths_visible = owner_is_admin_or_single_user(owner)
|
||||
agents_md = _context_info_agents_md_inventory(cwd) if _host_paths_visible else []
|
||||
# Workspace visibility: lets the TUI answer "can the backend actually
|
||||
# see this directory?" (mounted vs bridge-only) without probing.
|
||||
from src.workspace_paths import backend_workspace_path, workspace_mount_pairs
|
||||
@@ -1571,19 +1632,22 @@ def setup_session_routes(
|
||||
# Server-side tool policy: non-admin owners silently lose the computer
|
||||
# tools (src/tool_security); surface that so the TUI can show it.
|
||||
try:
|
||||
from src.tool_security import blocked_tools_for_owner
|
||||
from src.tool_security import blocked_tools_for_owner, delegated_credential_blocked_tools
|
||||
|
||||
_blocked = blocked_tools_for_owner(owner)
|
||||
if is_delegated_credential(request):
|
||||
_blocked.update(delegated_credential_blocked_tools())
|
||||
except Exception:
|
||||
_blocked = set()
|
||||
_computer = {"bash", "python", "read_file", "write_file", "host_shell"}
|
||||
_policy = {
|
||||
"computer_tools": "restricted" if _computer & _blocked else "full",
|
||||
"reason": "non-admin owner" if _blocked else "single-user or admin",
|
||||
"reason": "API-token caller" if is_delegated_credential(request) else
|
||||
"non-admin owner" if _blocked else "single-user or admin",
|
||||
}
|
||||
_workspace = {
|
||||
"backend_path": _backend_cwd,
|
||||
"exists_in_backend": bool(_backend_cwd) and Path(_backend_cwd).is_dir(),
|
||||
"exists_in_backend": _host_paths_visible and bool(_backend_cwd) and Path(_backend_cwd).is_dir(),
|
||||
"mount_configured": bool(workspace_mount_pairs()),
|
||||
"via_mount": bool(_raw_cwd) and backend_workspace_path(_raw_cwd) != _raw_cwd,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user