mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
fix(agent): enforce guide-only tool policy (#3088)
This commit is contained in:
+17
-5
@@ -277,11 +277,16 @@ def extract_preset(chat_handler, preset_id) -> PresetInfo:
|
||||
async def preprocess(
|
||||
chat_handler, message, att_ids, sess,
|
||||
auto_opened_docs: Optional[list] = None,
|
||||
allow_tool_preprocessing: bool = True,
|
||||
) -> PreprocessedMessage:
|
||||
"""Run chat_handler.preprocess_message and wrap the result."""
|
||||
enhanced, user_content, text_ctx, yt_transcripts, att_meta = (
|
||||
await chat_handler.preprocess_message(
|
||||
message, att_ids, sess, auto_opened_docs=auto_opened_docs
|
||||
message,
|
||||
att_ids,
|
||||
sess,
|
||||
auto_opened_docs=auto_opened_docs,
|
||||
allow_tool_preprocessing=allow_tool_preprocessing,
|
||||
)
|
||||
)
|
||||
return PreprocessedMessage(
|
||||
@@ -450,6 +455,7 @@ async def build_chat_context(
|
||||
webhook_manager=None,
|
||||
use_enhanced_message: bool = False,
|
||||
agent_mode: bool = False,
|
||||
allow_tool_preprocessing: bool = True,
|
||||
) -> ChatContext:
|
||||
"""Build the full context (preface + messages) for an LLM call.
|
||||
|
||||
@@ -467,6 +473,7 @@ async def build_chat_context(
|
||||
preprocessed = await preprocess(
|
||||
chat_handler, message, att_ids or [], sess,
|
||||
auto_opened_docs=auto_opened_docs,
|
||||
allow_tool_preprocessing=allow_tool_preprocessing,
|
||||
)
|
||||
|
||||
# Add user message to history
|
||||
@@ -485,6 +492,9 @@ async def build_chat_context(
|
||||
# Skills injection respects its own enable toggle (mirrors memory_enabled).
|
||||
# When off, the "Available skills" index is not added to the prompt.
|
||||
skills_enabled = not incognito and uprefs.get("skills_enabled", True)
|
||||
if not allow_tool_preprocessing:
|
||||
mem_enabled = False
|
||||
skills_enabled = False
|
||||
logger.debug(
|
||||
"Memory enabled=%s for user=%s (incognito=%s, no_memory=%s, pref=%s)",
|
||||
mem_enabled, user, incognito, no_memory, uprefs.get("memory_enabled", "NOT_SET"),
|
||||
@@ -492,11 +502,11 @@ async def build_chat_context(
|
||||
|
||||
# Use RAG?
|
||||
use_rag_val = (str(use_rag).lower() != "false") if use_rag is not None else True
|
||||
if incognito:
|
||||
if incognito or not allow_tool_preprocessing:
|
||||
use_rag_val = False
|
||||
|
||||
# If pre-fetched search context was provided (compare mode), skip live web search
|
||||
skip_web = bool(search_context)
|
||||
skip_web = bool(search_context) or not allow_tool_preprocessing
|
||||
|
||||
# Build context preface
|
||||
# The stream path uses enhanced_message (with CoT/preprocessing applied),
|
||||
@@ -523,7 +533,7 @@ async def build_chat_context(
|
||||
used_memories = getattr(chat_processor, '_last_used_memories', [])
|
||||
|
||||
# Inject pre-fetched search context (compare mode)
|
||||
if search_context:
|
||||
if search_context and allow_tool_preprocessing:
|
||||
preface.append(untrusted_context_message("prefetched search context", search_context))
|
||||
|
||||
# YouTube transcripts
|
||||
@@ -855,12 +865,13 @@ def run_post_response_tasks(
|
||||
skills_manager=None,
|
||||
owner: str = None,
|
||||
extract_skills: bool = True,
|
||||
allow_background_extraction: bool = True,
|
||||
):
|
||||
"""Fire background tasks after a completed response: memory extraction, webhooks, auto-name, skill extraction."""
|
||||
# Memory extraction — only every 4th message pair to avoid excess LLM calls
|
||||
_msg_count = len(sess.history) if hasattr(sess, 'history') else 0
|
||||
_should_extract = (_msg_count >= 4) and (_msg_count % 4 == 0)
|
||||
if not incognito and not compare_mode and _should_extract and uprefs.get("auto_memory", True):
|
||||
if allow_background_extraction and not incognito and not compare_mode and _should_extract and uprefs.get("auto_memory", True):
|
||||
from services.memory.memory_extractor import extract_and_store
|
||||
from src.task_endpoint import resolve_task_endpoint
|
||||
t_url, t_model, t_headers = resolve_task_endpoint(
|
||||
@@ -887,6 +898,7 @@ def run_post_response_tasks(
|
||||
)
|
||||
if (
|
||||
extract_skills
|
||||
and allow_background_extraction
|
||||
and auto_skills_enabled
|
||||
and not incognito
|
||||
and not compare_mode
|
||||
|
||||
+52
-12
@@ -40,6 +40,7 @@ from routes.chat_helpers import (
|
||||
_enforce_chat_privileges,
|
||||
)
|
||||
from src.action_intents import classify_tool_intent as _classify_tool_intent
|
||||
from src.tool_policy import build_effective_tool_policy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -305,8 +306,13 @@ def setup_chat_routes(
|
||||
# non-streaming path can't be used to bypass).
|
||||
_enforce_chat_privileges(request, sess)
|
||||
|
||||
tool_policy = build_effective_tool_policy(last_user_message=message)
|
||||
allow_tool_preprocessing = not tool_policy.block_all_tool_calls
|
||||
|
||||
# Inline memory command
|
||||
memory_response = await chat_handler.handle_memory_command(sess, message)
|
||||
memory_response = None
|
||||
if not tool_policy.blocks("manage_memory"):
|
||||
memory_response = await chat_handler.handle_memory_command(sess, message)
|
||||
if memory_response:
|
||||
return {"response": memory_response}
|
||||
|
||||
@@ -320,10 +326,15 @@ def setup_chat_routes(
|
||||
use_web=use_web,
|
||||
time_filter=time_filter,
|
||||
webhook_manager=webhook_manager,
|
||||
allow_tool_preprocessing=allow_tool_preprocessing,
|
||||
)
|
||||
|
||||
# Research injection
|
||||
if use_research:
|
||||
research_blocked_by_policy = (
|
||||
tool_policy.blocks("trigger_research")
|
||||
or tool_policy.blocks("manage_research")
|
||||
)
|
||||
if use_research and not research_blocked_by_policy:
|
||||
try:
|
||||
_r_ep, _r_model, _r_headers = _resolve_research_endpoint(sess)
|
||||
research_ctx = await research_handler.call_research_service(
|
||||
@@ -358,6 +369,7 @@ def setup_chat_routes(
|
||||
ctx.uprefs, memory_manager, memory_vector, webhook_manager,
|
||||
character_name=ctx.preset.character_name,
|
||||
owner=ctx.user,
|
||||
allow_background_extraction=not tool_policy.block_all_tool_calls,
|
||||
)
|
||||
|
||||
return {"response": reply}
|
||||
@@ -492,11 +504,6 @@ def setup_chat_routes(
|
||||
do_research = True
|
||||
logger.info(f"Session {session} in research_pending — auto-triggering research")
|
||||
|
||||
# Persist session mode (research > agent > chat)
|
||||
_effective_mode = 'research' if do_research else (chat_mode or 'chat')
|
||||
if _effective_mode in ('agent', 'research', 'chat'):
|
||||
set_session_mode(session, _effective_mode)
|
||||
|
||||
att_ids = []
|
||||
if body and isinstance(body.get("attachments"), list):
|
||||
att_ids = [str(x) for x in body["attachments"]]
|
||||
@@ -507,6 +514,10 @@ def setup_chat_routes(
|
||||
pass
|
||||
|
||||
no_memory = str(form_data.get("no_memory", "")).lower() == "true"
|
||||
pre_context_tool_policy = build_effective_tool_policy(
|
||||
last_user_message=message,
|
||||
)
|
||||
allow_tool_preprocessing = not pre_context_tool_policy.block_all_tool_calls
|
||||
|
||||
# Build shared context (stream path uses enhanced_message for context preface)
|
||||
ctx = await build_chat_context(
|
||||
@@ -528,6 +539,7 @@ def setup_chat_routes(
|
||||
# manage_skills (agent mode). In plain chat or incognito the
|
||||
# index would be useless / unwanted noise.
|
||||
agent_mode=(chat_mode == "agent"),
|
||||
allow_tool_preprocessing=allow_tool_preprocessing,
|
||||
)
|
||||
|
||||
_research_flags = {"do": do_research} # Mutable container for generator scope
|
||||
@@ -679,6 +691,25 @@ def setup_chat_routes(
|
||||
from src.tool_security import plan_mode_disabled_tools
|
||||
disabled_tools.update(plan_mode_disabled_tools())
|
||||
|
||||
tool_policy = build_effective_tool_policy(
|
||||
disabled_tools=disabled_tools,
|
||||
last_user_message=message,
|
||||
)
|
||||
disabled_tools = tool_policy.all_disabled_names()
|
||||
research_blocked_by_policy = bool(
|
||||
tool_policy.blocks("trigger_research")
|
||||
or tool_policy.blocks("manage_research")
|
||||
)
|
||||
effective_do_research = bool(
|
||||
do_research and _research_flags["do"] and not research_blocked_by_policy
|
||||
)
|
||||
|
||||
# Persist session mode after policy/privilege gates so blocked research
|
||||
# turns remain ordinary chat/agent streams and saved messages.
|
||||
_effective_mode = 'research' if effective_do_research else (chat_mode or 'chat')
|
||||
if _effective_mode in ('agent', 'research', 'chat'):
|
||||
set_session_mode(session, _effective_mode)
|
||||
|
||||
async def stream_with_save() -> AsyncGenerator[str, None]:
|
||||
# _effective_mode is read-only here; closure captures it from
|
||||
# the outer scope. (Was `nonlocal` but never reassigned.)
|
||||
@@ -686,7 +717,7 @@ def setup_chat_routes(
|
||||
web_sources = ctx.web_sources
|
||||
|
||||
# Register active stream for partial-save safety net
|
||||
_active_streams[session] = {"status": "streaming", "partial": "", "query": message, "is_research": do_research, "mode": _effective_mode}
|
||||
_active_streams[session] = {"status": "streaming", "partial": "", "query": message, "is_research": effective_do_research, "mode": _effective_mode}
|
||||
|
||||
if ctx.preprocessed.attachment_meta:
|
||||
yield f"data: {json.dumps({'type': 'attachments', 'data': ctx.preprocessed.attachment_meta})}\n\n"
|
||||
@@ -710,7 +741,7 @@ def setup_chat_routes(
|
||||
yield f"data: {json.dumps({'type': 'memories_used', 'data': ctx.used_memories})}\n\n"
|
||||
|
||||
# Run research as a background task (survives page refresh)
|
||||
if do_research and _research_flags["do"]:
|
||||
if effective_do_research:
|
||||
_r_ep, _r_model, _r_headers = _resolve_research_endpoint(sess)
|
||||
_auth_keys = list(_r_headers.keys()) if _r_headers else []
|
||||
logger.info(f"Research endpoint resolved: model={_r_model}, endpoint={_r_ep}, auth_keys={_auth_keys}, sess_headers_keys={list(sess.headers.keys()) if isinstance(sess.headers, dict) else type(sess.headers)}")
|
||||
@@ -849,7 +880,7 @@ def setup_chat_routes(
|
||||
_fallback_candidates = []
|
||||
|
||||
# Send model name early so the frontend can show it during streaming
|
||||
_model_suffix = "Research" if do_research else None
|
||||
_model_suffix = "Research" if effective_do_research else None
|
||||
_model_info = {"type": "model_info", "model": sess.model}
|
||||
if _model_suffix:
|
||||
_model_info["suffix"] = _model_suffix
|
||||
@@ -859,6 +890,12 @@ def setup_chat_routes(
|
||||
|
||||
if _is_image_generation_session(sess, owner=_user):
|
||||
from src.settings import get_setting
|
||||
if tool_policy.blocks("generate_image"):
|
||||
_blocked_msg = tool_policy.reason_for("generate_image")
|
||||
yield f'data: {json.dumps({"delta": _blocked_msg})}\n\n'
|
||||
yield "data: [DONE]\n\n"
|
||||
_active_streams.pop(session, None)
|
||||
return
|
||||
if not get_setting("image_gen_enabled", True):
|
||||
yield f'data: {json.dumps({"delta": "Image generation is disabled by the administrator."})}\n\n'
|
||||
yield "data: [DONE]\n\n"
|
||||
@@ -988,7 +1025,7 @@ def setup_chat_routes(
|
||||
rag_sources=ctx.rag_sources,
|
||||
research_sources=research_sources,
|
||||
used_memories=ctx.used_memories,
|
||||
do_research=do_research,
|
||||
do_research=effective_do_research,
|
||||
incognito=incognito,
|
||||
)
|
||||
if _saved_id:
|
||||
@@ -998,7 +1035,8 @@ def setup_chat_routes(
|
||||
last_metrics, ctx.uprefs, memory_manager, memory_vector, webhook_manager,
|
||||
incognito=incognito, compare_mode=compare_mode,
|
||||
character_name=ctx.preset.character_name,
|
||||
owner=_user,
|
||||
owner=_user,
|
||||
allow_background_extraction=not tool_policy.block_all_tool_calls,
|
||||
)
|
||||
_stream_set(session, status="done")
|
||||
yield chunk
|
||||
@@ -1052,6 +1090,7 @@ def setup_chat_routes(
|
||||
active_document=active_doc,
|
||||
session_id=session,
|
||||
disabled_tools=disabled_tools if disabled_tools else None,
|
||||
tool_policy=tool_policy,
|
||||
owner=_user,
|
||||
fallbacks=_fallback_candidates,
|
||||
workspace=workspace or None,
|
||||
@@ -1130,6 +1169,7 @@ def setup_chat_routes(
|
||||
skills_manager=skills_manager,
|
||||
owner=_user,
|
||||
extract_skills=user_requested_agent,
|
||||
allow_background_extraction=not tool_policy.block_all_tool_calls,
|
||||
)
|
||||
_stream_set(session, status="done")
|
||||
yield chunk
|
||||
|
||||
Reference in New Issue
Block a user