Merge remote-tracking branch 'origin/dev'

# Conflicts:
#	routes/document_routes.py
This commit is contained in:
pewdiepie-archdaemon
2026-07-01 10:11:22 +00:00
44 changed files with 3498 additions and 856 deletions
+6 -4
View File
@@ -14,6 +14,7 @@ Sub-modules:
import logging
from collections import namedtuple
from src.tool_security import BUILTIN_EMAIL_TOOLS
from src.tool_utils import _truncate, get_mcp_manager, set_mcp_manager
logger = logging.getLogger(__name__)
@@ -86,9 +87,10 @@ TOOL_TAGS = {"bash", "python", "web_search", "web_fetch", "read_file", "write_fi
"manage_endpoints", "manage_mcp", "manage_webhooks",
"manage_tokens", "manage_documents", "manage_settings",
"manage_notes", "manage_calendar",
"resolve_contact", "manage_contact", "list_email_accounts", "send_email", "list_emails",
"read_email", "reply_to_email", "bulk_email", "archive_email",
"delete_email", "mark_email_read",
"resolve_contact", "manage_contact",
# Email tool names come from BUILTIN_EMAIL_TOOLS (unioned below)
# so the fence regex, dispatch, and non-admin blocklist all cover
# the same set.
# Cookbook tools (LLM serving + downloads). Without these
# entries, native function calls to e.g. list_served_models
# are rejected as "Unknown function call" before reaching
@@ -105,7 +107,7 @@ TOOL_TAGS = {"bash", "python", "web_search", "web_fetch", "read_file", "write_fi
# Generic loopback to any UI-button endpoint (cookbook,
# gallery, email folders, etc.) — agent uses this when
# there's no named tool wrapper for the action.
"app_api"}
"app_api"} | BUILTIN_EMAIL_TOOLS
ToolBlock = namedtuple("ToolBlock", ["tool_type", "content"])
+9 -1
View File
@@ -14,6 +14,7 @@ import logging
from typing import Optional, Dict
from src.tool_utils import get_mcp_manager, _parse_tool_args
from src.tool_security import BUILTIN_EMAIL_TOOLS
logger = logging.getLogger(__name__)
@@ -706,7 +707,14 @@ async def do_manage_settings(content: str, owner: Optional[str] = None) -> Dict:
"tasks": ["manage_tasks"],
"notes": ["manage_notes"],
"calendar": ["manage_calendar"],
"email": ["mcp__email__list_emails", "mcp__email__read_email", "mcp__email__send_email"],
# The full built-in email tool set, in BOTH spellings: the
# qualified mcp__email__* names drive MCP schema hiding, the
# bare names drive function-schema hiding, and the runtime
# gate accepts either — deriving from BUILTIN_EMAIL_TOOLS
# keeps the toggle covering every tool the email server
# exposes instead of a hand-picked subset.
"email": sorted(BUILTIN_EMAIL_TOOLS)
+ [f"mcp__email__{t}" for t in sorted(BUILTIN_EMAIL_TOOLS)],
"research": ["web_search", "web_fetch"], # research is a per-request flag, not a tool (closest analog)
}
+43 -3
View File
@@ -186,6 +186,21 @@ class WriteFileTool:
lines = content.split("\n", 1)
raw_path = lines[0].strip()
body = lines[1] if len(lines) > 1 else ""
# Decode JSON-object args (the fenced inline-args shape
# ```write_file {"path": "...", "content": "..."}```), matching
# ReadFileTool above. Without this the whole JSON string becomes the
# path and the file is written under a garbage name. This is the live
# path: there is no filesystem MCP server, so write_file always runs
# here via _direct_fallback, not through _build_mcp_args.
_stripped = content.strip()
if _stripped.startswith("{"):
try:
_a = json.loads(_stripped)
if isinstance(_a, dict) and "path" in _a:
raw_path = str(_a.get("path", "")).strip()
body = str(_a.get("content", ""))
except (json.JSONDecodeError, TypeError, ValueError):
pass
try:
path = _resolve_tool_path(raw_path)
except ValueError as e:
@@ -288,11 +303,26 @@ class GlobTool:
base = os.path.abspath(root)
if not os.path.isdir(base):
return None, f"glob: {root}: not a directory"
rbase = os.path.realpath(base)
norm_pat = pattern.replace("\\", "/")
# Fast path: literal pattern (no wildcards) → direct path lookup.
if not any(c in norm_pat for c in "*?["):
cand = os.path.normpath(os.path.join(base, norm_pat))
if os.path.exists(cand):
cand = os.path.realpath(os.path.join(base, norm_pat))
# Keep the literal lookup inside the search root. os.path.join
# lets an absolute pattern (or one containing ../) escape `base`,
# which would turn glob into an existence/path oracle for
# arbitrary host files — bypassing the workspace/allowlist
# confinement that _resolve_search_root applies to the root.
# An escaping literal falls through to the walk, which only ever
# yields paths under base.
nbase = os.path.normcase(rbase)
try:
inside = cand == rbase or os.path.commonpath(
[os.path.normcase(cand), nbase]
) == nbase
except ValueError:
inside = False
if inside and os.path.exists(cand):
return [cand], None
# Literal not at exact path — fall through to walk so
# e.g. "foo.py" still matches at any depth (like rglob).
@@ -333,7 +363,13 @@ class GlobTool:
class GrepTool:
async def execute(self, content: str, ctx: dict) -> dict:
from src.tool_execution import _resolve_tool_path, _resolve_search_root, _truncate
from src.tool_execution import (
_SENSITIVE_FILE_PATTERNS,
_is_sensitive_path,
_resolve_tool_path,
_resolve_search_root,
_truncate,
)
args: Dict[str, Any] = {}
_s = (content or "").strip()
if _s.startswith("{"):
@@ -369,6 +405,8 @@ class GrepTool:
cmd.append("--ignore-case")
if glob_pat:
cmd += ["--glob", glob_pat]
for _pat in _SENSITIVE_FILE_PATTERNS:
cmd += ["--glob", f"!*{_pat}*"]
for _d in _CODENAV_SKIP_DIRS:
cmd += ["--glob", f"!**/{_d}/**"]
cmd += ["--regexp", pattern, root]
@@ -399,6 +437,8 @@ class GrepTool:
for fp in file_iter:
if len(hits) >= max_hits:
break
if _is_sensitive_path(os.path.realpath(fp)):
continue
try:
with open(fp, "r", encoding="utf-8", errors="strict") as f:
for i, line in enumerate(f, 1):