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
parent b3d43ad225
commit db29046e0b
9 changed files with 289 additions and 19 deletions
+91
View File
@@ -1187,6 +1187,97 @@ FUNCTION_TOOL_SCHEMAS = [
}
}
},
{
"type": "function",
"function": {
"name": "search_emails",
"description": "Search emails by free-text query (sender, subject, or body) across INBOX + Sent + Archive by default. Use whenever the user names a person or topic that isn't in the most recent inbox slice. Returns matching emails with UIDs for read_email/reply_to_email.",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Free-text query. Matches FROM, SUBJECT, and body TEXT."},
"folders": {"type": "array", "items": {"type": "string"}, "description": "Folders to search (default: INBOX, Sent, Archive)"},
"max_results": {"type": "integer", "description": "Max results per folder (default: 20)"},
"account": {"type": "string", "description": "Account name/email/id from list_email_accounts"},
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "draft_email",
"description": "Create a new email compose DRAFT document for user review — this does NOT send. Prefer this over send_email when the user asks you to write/compose an email without explicitly saying send.",
"parameters": {
"type": "object",
"properties": {
"to": {"type": "string", "description": "Recipient email address(es), comma-separated"},
"subject": {"type": "string", "description": "Email subject line"},
"body": {"type": "string", "description": "Draft body"},
"cc": {"type": "string", "description": "CC address(es), comma-separated (optional)"},
"bcc": {"type": "string", "description": "BCC address(es), comma-separated (optional)"},
"title": {"type": "string", "description": "Optional document title"},
"account": {"type": "string", "description": "Account name/email/id from list_email_accounts"},
},
"required": ["to", "subject", "body"]
}
}
},
{
"type": "function",
"function": {
"name": "draft_email_reply",
"description": "Create a threaded reply DRAFT document for an existing email UID — this does NOT send. Prefer this over reply_to_email when the user wants a reply written, not sent. Threads with In-Reply-To/References and prefills recipient/subject.",
"parameters": {
"type": "object",
"properties": {
"uid": {"type": "string", "description": "Exact Email UID from list_emails/read_email; never invent UID 1"},
"body": {"type": "string", "description": "Draft reply body text"},
"folder": {"type": "string", "description": "IMAP folder (default: INBOX)"},
"reply_all": {"type": "boolean", "description": "Reply to all recipients (default: false)"},
"title": {"type": "string", "description": "Optional document title"},
"account": {"type": "string", "description": "Account name/email/id from list_email_accounts"},
},
"required": ["uid", "body"]
}
}
},
{
"type": "function",
"function": {
"name": "ai_draft_email_reply",
"description": "Generate an AI-written reply draft for an email UID using the user's configured writing style, then open it as a compose document for review — this does NOT send. Use when asked to write/draft a reply without dictating the exact body.",
"parameters": {
"type": "object",
"properties": {
"uid": {"type": "string", "description": "Exact Email UID from list_emails/read_email; never invent UID 1"},
"folder": {"type": "string", "description": "IMAP folder (default: INBOX)"},
"reply_all": {"type": "boolean", "description": "Reply to all recipients (default: false)"},
"title": {"type": "string", "description": "Optional document title"},
"account": {"type": "string", "description": "Account name/email/id from list_email_accounts"},
},
"required": ["uid"]
}
}
},
{
"type": "function",
"function": {
"name": "download_attachment",
"description": "Download an email attachment to local disk and return the file path (read it with read_file afterwards). Use to review a document/spreadsheet/file attached to an email.",
"parameters": {
"type": "object",
"properties": {
"uid": {"type": "string", "description": "Email UID from list_emails"},
"index": {"type": "integer", "description": "Attachment index from read_email's attachments list"},
"folder": {"type": "string", "description": "IMAP folder (default: INBOX)"},
"account": {"type": "string", "description": "Account name/email/id from list_email_accounts"},
},
"required": ["uid", "index"]
}
}
},
]