fix(email): close remaining email-tool registry drift; classify every email tool for plan mode

Deep self-review follow-up on #3681. Three review rounds each found another
hand-maintained copy of the email tool list that had drifted; this commit
hunts down ALL remaining copies and pins them to BUILTIN_EMAIL_TOOLS.

The same 5 tools (search_emails, draft_email, draft_email_reply,
ai_draft_email_reply, download_attachment) were missing from every
advertising surface, so they were dispatchable but never offered:

- FUNCTION_TOOL_SCHEMAS: native function-calling models never saw them
  (the round-1 fix covered dispatch only); schemas added, mirroring the
  email server's inputSchema definitions.
- TOOL_SECTIONS: fenced-block models were never told about them; prompt
  sections added.
- tool_index: absent from the RAG embedding registry (never retrievable),
  the email keyword hints, and the scheduled assistant's always-available
  set — the latter two now derive from BUILTIN_EMAIL_TOOLS.
- agent_loop._DOMAIN_TOOL_MAP["email"], tool_policy._COMMON_TOOL_NAMES,
  the assistant tool-selector UI groups (assistant.js), and the default
  Assistant crew seed (task_scheduler) now derive from / cover the set.

Plan mode now classifies every email tool explicitly:

- list_email_accounts and search_emails join PLAN_MODE_READONLY_TOOLS.
  Without this, list_email_accounts sat in the plan-mode bare denylist
  (schema-derived) while its qualified form passed the MCP read-only
  filter — and the round-2 bare/qualified alias gate would have blocked
  the qualified call too, regressing read-only email discovery in plan
  mode.
- draft_email, draft_email_reply, ai_draft_email_reply, and
  download_attachment join the fail-closed mutator backstop (drafts
  create documents; download_attachment writes to disk).

Tests: tests/test_email_registry_sync.py pins every registry (including
the email server source and assistant.js) to BUILTIN_EMAIL_TOOLS and
asserts the plan-mode partition, so the next email tool can't drift; a
parse/strip mirror grid covers 192 fence shapes (tag x header x body)
asserting executed <=> stripped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
botinate
2026-06-11 22:19:51 +02:00
co-authored by Claude Fable 5
parent b3d43ad225
commit db29046e0b
9 changed files with 289 additions and 19 deletions
+5 -9
View File
@@ -7,6 +7,8 @@ from dataclasses import dataclass, field
from types import MappingProxyType
from typing import Iterable, Mapping, Optional, Set, Tuple
from src.tool_security import BUILTIN_EMAIL_TOOLS
GUIDE_ONLY_DIRECTIVE = (
"## GUIDE-ONLY MODE - TOOL POLICY\n"
@@ -17,20 +19,19 @@ GUIDE_ONLY_DIRECTIVE = (
)
# Email tool names come from BUILTIN_EMAIL_TOOLS (unioned below) so this
# best-effort known-names set can't drift from what the email server exposes.
_COMMON_TOOL_NAMES = {
"api_call",
"app_api",
"archive_email",
"ask_teacher",
"ask_user",
"bash",
"bulk_email",
"builtin_browser",
"cancel_download",
"chat_with_model",
"create_document",
"create_session",
"delete_email",
"download_model",
"edit_document",
"edit_file",
@@ -41,7 +42,6 @@ _COMMON_TOOL_NAMES = {
"list_cached_models",
"list_cookbook_servers",
"list_downloads",
"list_emails",
"list_models",
"list_serve_presets",
"list_served_models",
@@ -61,16 +61,12 @@ _COMMON_TOOL_NAMES = {
"manage_tasks",
"manage_tokens",
"manage_webhooks",
"mark_email_read",
"pipeline",
"python",
"read_email",
"read_file",
"reply_to_email",
"resolve_contact",
"search_chats",
"search_hf_models",
"send_email",
"send_to_session",
"serve_model",
"serve_preset",
@@ -86,7 +82,7 @@ _COMMON_TOOL_NAMES = {
"web_fetch",
"web_search",
"write_file",
}
} | BUILTIN_EMAIL_TOOLS
_GUIDE_ONLY_PATTERNS: Tuple[Tuple[re.Pattern[str], str], ...] = tuple(