mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-11 12:27:13 +00:00
Merge remote-tracking branch 'origin/dev'
# Conflicts: # routes/document_routes.py
This commit is contained in:
+93
-3
@@ -21,7 +21,12 @@ from typing import Any, Awaitable, Callable, Dict, Optional, Tuple
|
||||
|
||||
|
||||
|
||||
from src.tool_security import is_public_blocked_tool, owner_is_admin_or_single_user
|
||||
from src.tool_security import (
|
||||
BUILTIN_EMAIL_TOOLS,
|
||||
email_tool_policy_names,
|
||||
is_public_blocked_tool,
|
||||
owner_is_admin_or_single_user,
|
||||
)
|
||||
from src.tool_policy import ToolPolicy
|
||||
from src.constants import MAX_OUTPUT_CHARS, MAX_READ_CHARS, MAX_DIFF_LINES, DATA_DIR
|
||||
from src.tool_utils import _truncate, get_mcp_manager
|
||||
@@ -390,8 +395,42 @@ _MCP_ARG_PARSERS: Dict[str, Callable[[str], Dict[str, str]]] = {
|
||||
}
|
||||
|
||||
|
||||
# Primary argument key(s) for the legacy line-parsed tools. When a fenced
|
||||
# block's content is a JSON object carrying one of these keys, it's structured
|
||||
# inline args (the relaxed parser's ```web_search {"query": "..."}``` shape) —
|
||||
# use the object directly instead of letting the line-based parsers wrap the
|
||||
# whole JSON string as the query/url/path/prompt. Keyed off membership only
|
||||
# (the primary key never changes), so this can't drift; an unrecognized object
|
||||
# safely falls through to the line-based parser, i.e. the previous behavior.
|
||||
#
|
||||
# IMPORTANT — this only covers the MCP path. _build_mcp_args is reached via
|
||||
# _call_mcp_tool only for _MCP_TOOL_MAP tools (so an entry outside that map is
|
||||
# dead, as manage_memory was). And of these, only generate_image has a live MCP
|
||||
# server today; web_search/web_fetch/read_file/write_file have none, so they run
|
||||
# via _direct_fallback -> TOOL_HANDLERS, whose handlers decode JSON themselves
|
||||
# (see ReadFileTool/WriteFileTool/WebSearchTool/WebFetchTool). The entries here
|
||||
# are kept as defense-in-depth for if/when those servers are added. The live
|
||||
# fix for each server-less tool lives in its handler. test_write_file_inline_
|
||||
# json_args and test_mcp_json_primary_keys_are_all_live pin both halves.
|
||||
_MCP_JSON_PRIMARY_KEYS: Dict[str, tuple] = {
|
||||
"web_search": ("query", "queries"),
|
||||
"web_fetch": ("url",),
|
||||
"read_file": ("path",),
|
||||
"write_file": ("path",),
|
||||
"generate_image": ("prompt",),
|
||||
}
|
||||
|
||||
|
||||
def _build_mcp_args(tool: str, content: str) -> Dict:
|
||||
"""Convert fenced-block text content to structured MCP arguments."""
|
||||
primaries = _MCP_JSON_PRIMARY_KEYS.get(tool)
|
||||
if primaries and content.strip().startswith("{"):
|
||||
try:
|
||||
decoded = json.loads(content.strip())
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
decoded = None
|
||||
if isinstance(decoded, dict) and any(k in decoded for k in primaries):
|
||||
return decoded
|
||||
parser = _MCP_ARG_PARSERS.get(tool)
|
||||
return parser(content) if parser else {}
|
||||
|
||||
@@ -596,6 +635,12 @@ async def _execute_tool_block_impl(
|
||||
tool = block.tool_type
|
||||
content = block.content
|
||||
|
||||
# The block/disable gates below must match every policy-equivalent
|
||||
# spelling of the tool name (bare email names alias their mcp__email__
|
||||
# form — see email_tool_policy_names), not just the spelling the model
|
||||
# happened to emit.
|
||||
policy_names = email_tool_policy_names(tool)
|
||||
|
||||
# Misformatted tool call detection: model put JSON inside ```python``` (or
|
||||
# similar) without naming the tool. Common with MiniMax-style outputs.
|
||||
# Return a helpful error so the model retries with the correct format.
|
||||
@@ -623,13 +668,13 @@ async def _execute_tool_block_impl(
|
||||
pass
|
||||
|
||||
# Reject tools that the user has disabled for this request
|
||||
if disabled_tools and tool in disabled_tools:
|
||||
if disabled_tools and not policy_names.isdisjoint(disabled_tools):
|
||||
desc = f"{tool}: BLOCKED"
|
||||
result = {"error": f"Tool '{tool}' is disabled by user.", "exit_code": 1}
|
||||
logger.info(f"Tool blocked by user: {tool}")
|
||||
return desc, result
|
||||
|
||||
if tool_policy and tool_policy.blocks(tool):
|
||||
if tool_policy and any(tool_policy.blocks(name) for name in policy_names):
|
||||
desc = f"{tool}: BLOCKED"
|
||||
result = {
|
||||
"error": f"Execution of tool '{tool}' is forbade by the active guide-only policy.",
|
||||
@@ -823,6 +868,51 @@ async def _execute_tool_block_impl(
|
||||
elif tool == "vault_unlock":
|
||||
desc = "vault_unlock"
|
||||
result = await do_vault_unlock(content, owner=owner)
|
||||
elif tool in BUILTIN_EMAIL_TOOLS:
|
||||
# Bare email tool name from fenced-block models (e.g. Ollama) — route to MCP email server.
|
||||
# Non-admin owners never reach here: BUILTIN_EMAIL_TOOLS ⊆ NON_ADMIN_BLOCKED_TOOLS,
|
||||
# so is_public_blocked_tool() above already rejected them.
|
||||
mcp = get_mcp_manager()
|
||||
qualified = f"mcp__email__{tool}"
|
||||
desc = f"email: {tool}"
|
||||
if mcp:
|
||||
_raw = content.strip()
|
||||
args = {}
|
||||
_args_error = None
|
||||
if _raw:
|
||||
# A non-empty body is always meant to be the call's arguments,
|
||||
# and every email tool takes a JSON object. Anything that
|
||||
# isn't one is a correctable error — NOT a silent empty-args
|
||||
# call, which would read the DEFAULT mailbox/folder instead of
|
||||
# the one the model meant (#3966 class). Only an EMPTY body
|
||||
# keeps the no-arg path (e.g. ```list_email_accounts```).
|
||||
try:
|
||||
parsed = json.loads(_raw)
|
||||
except (json.JSONDecodeError, TypeError) as _je:
|
||||
# Covers both `{account: "work"}` (looks like JSON, bad)
|
||||
# and `account: work` (not JSON at all).
|
||||
_args_error = (
|
||||
f"'{tool}' arguments are not valid JSON ({_je}). "
|
||||
'Send a JSON object, e.g. {"account": "work"} — '
|
||||
"keys and string values need double quotes."
|
||||
)
|
||||
else:
|
||||
if isinstance(parsed, dict):
|
||||
args = parsed
|
||||
else:
|
||||
_args_error = (
|
||||
f"'{tool}' arguments must be a JSON object, "
|
||||
'e.g. {"uid": "..."} — got a JSON array/value instead.'
|
||||
)
|
||||
if _args_error is not None:
|
||||
result = {"error": _args_error, "exit_code": 1}
|
||||
else:
|
||||
if owner:
|
||||
args = dict(args)
|
||||
args[_EMAIL_MCP_OWNER_ARG] = owner
|
||||
result = await mcp.call_tool(qualified, args)
|
||||
else:
|
||||
result = {"error": "MCP manager not available", "exit_code": 1}
|
||||
elif tool.startswith("mcp__"):
|
||||
# MCP tool dispatch
|
||||
mcp = get_mcp_manager()
|
||||
|
||||
Reference in New Issue
Block a user