fix(agent): enforce guide-only tool policy (#3088)

This commit is contained in:
Nicholai
2026-06-06 18:48:24 -06:00
committed by GitHub
parent 108ee1e32b
commit a3cb15d0a1
9 changed files with 993 additions and 207 deletions
+52 -12
View File
@@ -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