mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-11-04 20:22:21 +01:00
fix(harness): ship coherent vendor harness cluster
Rebuild PR C to ship the vendor/photocraft harness as a coherent unit (vendor implementation + vendor tests) instead of a per-file ahead/behind mix that produced broken imports and 6+ test failures. - Flip 36 harness impl/route/test files back to vendor (the per-file line-set policy misclassified user-rewritten files as 'behind'). - Flip core/models.py + src/tool_approval_scopes.py to vendor: vendor's harness (authority.py, turn_contract.py, clean_agent_preview.py) depends on vendor's older approval-scope API; upstream added CHAT_SESSION_APPROVAL_SIGNATURE_FIELD that vendor's core/models.py lacks. - Add src/shutdown.py (new vendor infra) required by routes/chat_routes.py. - Keep src/agent_tools/__init__.py at upstream (editor_tools registration lives in PR B). Upstream-only security divergence preserved in C_upstream_delta.patch and flagged for integration-time reconciliation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
364229f2dc
commit
9bdea753ae
@@ -16,7 +16,6 @@ from pydantic import BaseModel
|
||||
|
||||
from core.database import SessionLocal, CrewMember, ScheduledTask
|
||||
from src.auth_helpers import get_current_user
|
||||
from src.endpoint_resolver import resolve_owner_registered_endpoint_url
|
||||
from src.owner_identity import REQUEST_SENTINEL_OWNERS
|
||||
from src.task_scheduler import compute_next_run
|
||||
|
||||
@@ -179,13 +178,7 @@ def setup_assistant_routes(task_scheduler) -> APIRouter:
|
||||
if payload.model is not None:
|
||||
crew_db.model = payload.model or None
|
||||
if payload.endpoint_url is not None:
|
||||
try:
|
||||
crew_db.endpoint_url = (
|
||||
resolve_owner_registered_endpoint_url(db, payload.endpoint_url, owner)
|
||||
if payload.endpoint_url else None
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
crew_db.endpoint_url = payload.endpoint_url or None
|
||||
if payload.timezone is not None:
|
||||
crew_db.timezone = payload.timezone or None
|
||||
|
||||
|
||||
@@ -98,9 +98,7 @@ def clean_repeated_assistant_content(text: object) -> str:
|
||||
"",
|
||||
value,
|
||||
).strip()
|
||||
# No leading `\s*`: .strip() removes that whitespace anyway, and scanning
|
||||
# it from every offset of a long whitespace run was quadratic (ReDoS).
|
||||
value = re.sub(r"(?is)</\s*think\s*>\s*$", "", value).strip()
|
||||
value = re.sub(r"(?is)\s*</\s*think\s*>\s*$", "", value).strip()
|
||||
return value
|
||||
|
||||
_CASUAL_OPENING_RE = re.compile(
|
||||
@@ -1326,17 +1324,13 @@ def _normalize_thinking(text: str) -> str:
|
||||
|
||||
# Handle garbled <think> tags: reasoning text followed by <think> as separator
|
||||
# e.g. "The user said...I should respond.\n<think>Hey! What's up?"
|
||||
# Linear form of `^([\s\S]+?)\n*<think>\s*([\s\S]*?)(?:</think>)?\s*$`:
|
||||
# the lookbehind stops the lazy prefix re-scanning a newline run from every
|
||||
# offset, and the optional trailing closer is dropped after the match
|
||||
# instead of being retried at every body offset (both were quadratic).
|
||||
garbled = re.match(
|
||||
r'^([\s\S]+?)(?<![\s\S]\n)\n*<think(?:ing)?>\s*([\s\S]*)$',
|
||||
r'^([\s\S]+?)\n*<think(?:ing)?>\s*([\s\S]*?)(?:</think(?:ing)?>)?\s*$',
|
||||
text, re.IGNORECASE
|
||||
)
|
||||
if garbled:
|
||||
before = garbled.group(1).strip()
|
||||
after = re.sub(r'</think(?:ing)?>$', '', garbled.group(2).rstrip(), flags=re.IGNORECASE).strip()
|
||||
after = garbled.group(2).strip()
|
||||
# Only treat as garbled if the part before <think> looks like reasoning
|
||||
reasoning_starts = (
|
||||
'The user ', 'I need ', 'I should ', 'I will ',
|
||||
|
||||
@@ -701,17 +701,13 @@ def setup_cookbook_routes() -> APIRouter:
|
||||
return cmd
|
||||
|
||||
repo_id = "cyankiwi/MiniMax-M3-AWQ-INT4"
|
||||
hf_home = Path(os.environ.get("HF_HOME", str(Path.home() / ".cache" / "huggingface")))
|
||||
hf_cache = Path(os.environ.get("HUGGINGFACE_HUB_CACHE", str(hf_home / "hub")))
|
||||
snapshot_root = hf_cache / "models--cyankiwi--MiniMax-M3-AWQ-INT4" / "snapshots"
|
||||
if body[serve_i + 1] == repo_id and snapshot_root.is_dir():
|
||||
installed_snapshots = sorted(
|
||||
(p for p in snapshot_root.iterdir() if p.is_dir()),
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
if installed_snapshots:
|
||||
body[serve_i + 1] = str(installed_snapshots[0])
|
||||
snapshot = (
|
||||
"/home/pewds/.cache/huggingface/hub/"
|
||||
"models--cyankiwi--MiniMax-M3-AWQ-INT4/"
|
||||
"snapshots/4082acbbec1236d21828d55b6bb0fe02ade4ab5b"
|
||||
)
|
||||
if body[serve_i + 1] == repo_id:
|
||||
body[serve_i + 1] = snapshot
|
||||
|
||||
def add_env(key: str, value: str) -> None:
|
||||
if not any(p.startswith(f"{key}=") for p in env_parts):
|
||||
|
||||
@@ -7,14 +7,13 @@ import logging
|
||||
import re
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException, Depends
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
|
||||
from core.models import ChatMessage
|
||||
from core.database import SessionLocal, ChatMessage as DbChatMessage, Session as DbSession
|
||||
from src.auth_helpers import effective_user, require_chat_api_token_scope
|
||||
from src.auth_helpers import effective_user
|
||||
from src.topic_analyzer import analyze_topics
|
||||
from src.upload_handler import reserve_message_upload_references
|
||||
from src.tool_approval_scopes import sanitize_client_message_metadata
|
||||
from routes.session_routes import (
|
||||
_message_role,
|
||||
_message_text,
|
||||
@@ -238,10 +237,7 @@ def _keep_count_before_message(db_messages, before_msg_id: str | None) -> int |
|
||||
|
||||
|
||||
def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
router = APIRouter(
|
||||
tags=["history"],
|
||||
dependencies=[Depends(require_chat_api_token_scope)],
|
||||
)
|
||||
router = APIRouter(tags=["history"])
|
||||
|
||||
def _reserve_message_uploads(
|
||||
request: Request,
|
||||
@@ -436,7 +432,7 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter:
|
||||
content = body.get("content", "")
|
||||
if not content:
|
||||
raise HTTPException(400, "content is required")
|
||||
metadata = sanitize_client_message_metadata(body.get("metadata"))
|
||||
metadata = body.get("metadata")
|
||||
_reserve_message_uploads(request, content, metadata)
|
||||
msg = ChatMessage(role=role, content=content, metadata=metadata)
|
||||
session_manager.add_message(session_id, msg)
|
||||
|
||||
@@ -43,22 +43,11 @@ def setup_search_routes(config) -> APIRouter:
|
||||
router = APIRouter(tags=["search"])
|
||||
|
||||
@router.get("/search/web", response_class=HTMLResponse)
|
||||
async def web_search_page(request: Request, q: str = Query("", min_length=0)) -> HTMLResponse:
|
||||
async def web_search_page(q: str = Query("", min_length=0)) -> HTMLResponse:
|
||||
"""Browser-facing search results page for clickable agent web_search rows."""
|
||||
# The site CSP allows inline script only with the per-request nonce
|
||||
# (SecurityHeadersMiddleware); without it this page's script never ran.
|
||||
nonce = html.escape(getattr(request.state, "csp_nonce", "") or "", quote=True)
|
||||
safe_q = str(q or "").strip()
|
||||
title = html.escape(safe_q or "Web search")
|
||||
# json.dumps leaves `<`, `>` and `&` alone, so q=</script><script ...>
|
||||
# would close this inline <script> and inject markup (reflected XSS).
|
||||
# Unicode-escape them; the JS string value is unchanged.
|
||||
q_json = (
|
||||
json.dumps(safe_q)
|
||||
.replace("<", "\\u003c")
|
||||
.replace(">", "\\u003e")
|
||||
.replace("&", "\\u0026")
|
||||
)
|
||||
q_json = json.dumps(safe_q)
|
||||
page = f"""<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -90,7 +79,7 @@ def setup_search_routes(config) -> APIRouter:
|
||||
<div class="status" id="status">Loading...</div>
|
||||
<div id="results"></div>
|
||||
</main>
|
||||
<script nonce="{nonce}">
|
||||
<script>
|
||||
const initialQuery = {q_json};
|
||||
const input = document.getElementById('query');
|
||||
const statusEl = document.getElementById('status');
|
||||
@@ -114,9 +103,7 @@ def setup_search_routes(config) -> APIRouter:
|
||||
}}
|
||||
statusEl.textContent = sources.length ? `${{sources.length}} results` : 'No results';
|
||||
resultsEl.innerHTML = sources.map(s => {{
|
||||
const rawUrl = String(s.url || s.link || '');
|
||||
// Result URLs come from third-party search results: only http(s) may become a link.
|
||||
const url = /^https?:[/][/]/i.test(rawUrl) ? rawUrl : '';
|
||||
const url = s.url || s.link || '';
|
||||
const title = s.title || url || 'Untitled';
|
||||
const snippet = s.snippet || s.content || '';
|
||||
return `<a class="result" href="${{esc(url)}}" target="_blank" rel="noopener noreferrer">
|
||||
|
||||
@@ -6,24 +6,17 @@ import uuid
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from fastapi import APIRouter, Form, HTTPException, Response, Request, Depends, Query
|
||||
from fastapi import APIRouter, Form, HTTPException, Response, Request, 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,
|
||||
is_delegated_credential,
|
||||
require_chat_api_token_scope,
|
||||
)
|
||||
from src.auth_helpers import effective_user, _auth_disabled, owner_filter
|
||||
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:
|
||||
@@ -241,15 +234,9 @@ def _verify_session_owner(request: Request, session_id: str, session_manager=Non
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api",
|
||||
tags=["sessions"],
|
||||
dependencies=[Depends(require_chat_api_token_scope)],
|
||||
)
|
||||
router = APIRouter(prefix="/api", tags=["sessions"])
|
||||
|
||||
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)
|
||||
@@ -280,22 +267,6 @@ 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)
|
||||
@@ -530,11 +501,6 @@ 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)
|
||||
@@ -791,11 +757,7 @@ 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=sanitize_client_message_metadata(m.get("metadata")),
|
||||
))
|
||||
sess.add_message(ChatMessage(m["role"], m["content"], metadata=m.get("metadata")))
|
||||
session_manager.save_sessions()
|
||||
return {"ok": True, "count": len(messages)}
|
||||
|
||||
@@ -1156,8 +1118,6 @@ 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())
|
||||
@@ -1601,13 +1561,7 @@ def setup_session_routes(
|
||||
raise HTTPException(404, "Session not found")
|
||||
skills = _context_info_skill_inventory(skills_manager, owner=owner)
|
||||
tools = _context_info_tool_inventory()
|
||||
# `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 []
|
||||
agents_md = _context_info_agents_md_inventory(cwd)
|
||||
# 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
|
||||
@@ -1617,22 +1571,19 @@ 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, delegated_credential_blocked_tools
|
||||
from src.tool_security import blocked_tools_for_owner
|
||||
|
||||
_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": "API-token caller" if is_delegated_credential(request) else
|
||||
"non-admin owner" if _blocked else "single-user or admin",
|
||||
"reason": "non-admin owner" if _blocked else "single-user or admin",
|
||||
}
|
||||
_workspace = {
|
||||
"backend_path": _backend_cwd,
|
||||
"exists_in_backend": _host_paths_visible and bool(_backend_cwd) and Path(_backend_cwd).is_dir(),
|
||||
"exists_in_backend": 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,
|
||||
}
|
||||
|
||||
+15
-22
@@ -19,7 +19,7 @@ from pydantic import BaseModel, Field
|
||||
from services.memory.skills import SkillsManager
|
||||
from src.auth_helpers import get_current_user
|
||||
from src.prompt_security import untrusted_context_message
|
||||
from src.text_helpers import strip_closed_think_blocks
|
||||
from src.model_profiles import uses_odysseus_progressive_thinking
|
||||
from core.middleware import require_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -251,7 +251,7 @@ async def _eval_skill_run(skill_md: str, task: str, transcript: str,
|
||||
# Strip closed think blocks. If a <think> was opened but never closed
|
||||
# (the model ran out of budget mid-reasoning), drop everything from it
|
||||
# onward so its stray braces don't poison JSON extraction.
|
||||
text = strip_closed_think_blocks(text)
|
||||
text = _re.sub(r'<think(?:ing)?>[\s\S]*?</think(?:ing)?>', '', text, flags=_re.I)
|
||||
text = _re.sub(r'<think(?:ing)?>[\s\S]*$', '', text, flags=_re.I).strip()
|
||||
|
||||
def _coerce(d):
|
||||
@@ -348,6 +348,8 @@ async def _eval_skill_run(skill_md: str, task: str, transcript: str,
|
||||
url, model, msgs,
|
||||
temperature=0.1, max_tokens=32768, headers=headers, timeout=180,
|
||||
workload=workload,
|
||||
thinking_mode=('on' if attempt == 0 else 'off')
|
||||
if uses_odysseus_progressive_thinking(model) else None,
|
||||
)
|
||||
except Exception as e:
|
||||
# Don't give up on a transient first-attempt error — let the second
|
||||
@@ -400,11 +402,12 @@ async def _eval_skill_necessity(skill_md: str, others: list, url: str, model: st
|
||||
[{"role": "system", "content": sys_prompt}, {"role": "user", "content": user_msg}],
|
||||
temperature=0.1, max_tokens=8192, headers=headers, timeout=120,
|
||||
workload=workload,
|
||||
thinking_mode='on' if uses_odysseus_progressive_thinking(model) else None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Necessity check failed: {e}")
|
||||
return None
|
||||
text = strip_closed_think_blocks(raw or '')
|
||||
text = _re.sub(r'<think(?:ing)?>[\s\S]*?</think(?:ing)?>', '', (raw or ''), flags=_re.I)
|
||||
text = _re.sub(r'<think(?:ing)?>[\s\S]*$', '', text, flags=_re.I).strip()
|
||||
data = None
|
||||
a, b = text.find('{'), text.rfind('}')
|
||||
@@ -490,11 +493,12 @@ async def _eval_skill_retrieval_precision(skill_md: str, others: list,
|
||||
[{"role": "system", "content": sys_prompt}, {"role": "user", "content": user_msg}],
|
||||
temperature=0.1, max_tokens=4096, headers=headers, timeout=90,
|
||||
workload=workload,
|
||||
thinking_mode='on' if uses_odysseus_progressive_thinking(model) else None,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Retrieval precision check failed: {e}")
|
||||
return None
|
||||
text = strip_closed_think_blocks(raw or '')
|
||||
text = _re.sub(r'<think(?:ing)?>[\s\S]*?</think(?:ing)?>', '', (raw or ''), flags=_re.I)
|
||||
text = _re.sub(r'<think(?:ing)?>[\s\S]*$', '', text, flags=_re.I).strip()
|
||||
data = None
|
||||
a, b = text.find('{'), text.rfind('}')
|
||||
@@ -1185,11 +1189,12 @@ async def _improve_skill_md(skill_md: str, verdict: dict, transcript: str, url,
|
||||
[{"role": "system", "content": sys_prompt},
|
||||
{"role": "user", "content": user_msg}],
|
||||
temperature=0.2, max_tokens=16384, headers=headers, timeout=180,
|
||||
workload=workload)
|
||||
workload=workload,
|
||||
thinking_mode='on' if uses_odysseus_progressive_thinking(model) else None)
|
||||
except Exception as e:
|
||||
logger.warning(f"Audit: improve call failed: {e}")
|
||||
return None
|
||||
text = strip_closed_think_blocks(raw or '')
|
||||
text = _re.sub(r'<think(?:ing)?>[\s\S]*?</think(?:ing)?>', '', (raw or ''), flags=_re.I)
|
||||
text = _re.sub(r'<think(?:ing)?>[\s\S]*$', '', text, flags=_re.I)
|
||||
text = _re.sub(r'</think(?:ing)?>', '', text, flags=_re.I).strip()
|
||||
if text.startswith("```"):
|
||||
@@ -2004,11 +2009,7 @@ def setup_skills_routes(skills_manager: SkillsManager) -> APIRouter:
|
||||
it untouched). It never changes the skill's published/draft STATUS."""
|
||||
import time as _time
|
||||
import asyncio as _asyncio
|
||||
from core.database import SessionLocal
|
||||
from src.endpoint_resolver import (
|
||||
build_chat_url, build_headers, resolve_endpoint,
|
||||
resolve_endpoint_runtime, resolve_owner_registered_endpoint,
|
||||
)
|
||||
from src.endpoint_resolver import resolve_endpoint
|
||||
|
||||
user = _owner(request)
|
||||
body = await request.json()
|
||||
@@ -2032,18 +2033,10 @@ def setup_skills_routes(skills_manager: SkillsManager) -> APIRouter:
|
||||
# session's model. Fall back to the caller's session model only if unset.
|
||||
url, model, headers = resolve_endpoint("utility", owner=user)
|
||||
if not url or not model:
|
||||
if not url and body.get("endpoint_url") is not None:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
endpoint = resolve_owner_registered_endpoint(db, body["endpoint_url"], user)
|
||||
base, api_key = resolve_endpoint_runtime(endpoint, owner=user)
|
||||
url = build_chat_url(base)
|
||||
headers = build_headers(api_key, base)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
finally:
|
||||
db.close()
|
||||
url = url or ((body.get("endpoint_url") or "").strip() or None)
|
||||
model = model or ((body.get("model") or "").strip() or None)
|
||||
if headers is None and isinstance(body.get("headers"), dict):
|
||||
headers = body.get("headers")
|
||||
if not url or not model:
|
||||
raise HTTPException(400, "No model configured — set a Default or Utility model in Settings.")
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ from core.database import SessionLocal, ScheduledTask, TaskRun, NotificationLog
|
||||
from core.constants import internal_api_base
|
||||
from src.auth_helpers import get_current_user
|
||||
from src.constants import DATA_DIR, EMAIL_URGENCY_CACHE_DIR
|
||||
from src.endpoint_resolver import resolve_owner_registered_endpoint_url
|
||||
from src.task_action_policy import (
|
||||
ADMIN_ONLY_TASK_ACTIONS,
|
||||
is_admin_only_task_action,
|
||||
@@ -520,12 +519,6 @@ def setup_task_routes(task_scheduler) -> APIRouter:
|
||||
db = SessionLocal()
|
||||
try:
|
||||
then_task_id = _validate_then_task_id(db, req.then_task_id, user)
|
||||
endpoint_url = None
|
||||
if req.endpoint_url:
|
||||
try:
|
||||
endpoint_url = resolve_owner_registered_endpoint_url(db, req.endpoint_url, user)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
notifications_enabled = (
|
||||
False if req.task_type == "action" and req.notifications_enabled is None
|
||||
else bool(req.notifications_enabled) if req.notifications_enabled is not None
|
||||
@@ -562,7 +555,7 @@ def setup_task_routes(task_scheduler) -> APIRouter:
|
||||
status="active" if (req.trigger_type in ("event", "webhook") or next_run) else "completed",
|
||||
output_target=req.output_target,
|
||||
model=req.model or None,
|
||||
endpoint_url=endpoint_url,
|
||||
endpoint_url=req.endpoint_url or None,
|
||||
then_task_id=then_task_id,
|
||||
webhook_token=webhook_token,
|
||||
notifications_enabled=notifications_enabled,
|
||||
@@ -618,8 +611,12 @@ def setup_task_routes(task_scheduler) -> APIRouter:
|
||||
async def create_notification_log(request: Request):
|
||||
"""Persist an in-app toast so Settings can show notification history."""
|
||||
user = _owner(request)
|
||||
# Toast logging is best-effort. With auth disabled (or no user) there is
|
||||
# nothing meaningful to persist, so return success rather than a 401 —
|
||||
# a 401 here would trip the global fetch redirect-to-/login handler and
|
||||
# yank users out of the editor mid-edit.
|
||||
if not user:
|
||||
raise HTTPException(401, "Authentication required")
|
||||
return {"success": False}
|
||||
body = await request.json()
|
||||
message = str(body.get("body") or "").strip()[:2000]
|
||||
if not message:
|
||||
@@ -762,14 +759,7 @@ def setup_task_routes(task_scheduler) -> APIRouter:
|
||||
if req.model is not None:
|
||||
task.model = req.model or None
|
||||
if req.endpoint_url is not None:
|
||||
try:
|
||||
# An empty override restores the existing default-model workflow.
|
||||
task.endpoint_url = (
|
||||
resolve_owner_registered_endpoint_url(db, req.endpoint_url, user)
|
||||
if req.endpoint_url else None
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
task.endpoint_url = req.endpoint_url or None
|
||||
if req.trigger_type is not None:
|
||||
# Generate webhook token when switching to webhook trigger
|
||||
if req.trigger_type == "webhook" and not task.webhook_token:
|
||||
@@ -1252,14 +1242,8 @@ def setup_task_routes(task_scheduler) -> APIRouter:
|
||||
if not out.get("prompt"):
|
||||
return {"success": False, "message": "Could not extract a task instruction"}
|
||||
return {"success": True, "draft": out}
|
||||
except HTTPException as e:
|
||||
# llm_core raises HTTPException with curated upstream text.
|
||||
logger.error(f"parse_task failed: {e.detail}")
|
||||
return {"success": False, "message": str(e.detail)}
|
||||
except Exception as e:
|
||||
# Anything else (JSON/parse/runtime errors) can carry internals;
|
||||
# log it and keep the client message generic.
|
||||
logger.error(f"parse_task failed: {e}")
|
||||
return {"success": False, "message": f"Could not draft a task ({type(e).__name__})"}
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
return router
|
||||
|
||||
Reference in New Issue
Block a user