mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-11 12:27:13 +00:00
db29046e0b
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>
123 lines
4.9 KiB
Python
123 lines
4.9 KiB
Python
"""PR #3681 — every email-tool registry derives from / covers BUILTIN_EMAIL_TOOLS.
|
|
|
|
Three review rounds on #3681 each found another hand-maintained copy of the
|
|
email tool list that had drifted (NON_ADMIN_BLOCKED_TOOLS, the `disable_tool
|
|
email` alias, tool_schemas' private copy). This test pins ALL of them to the
|
|
single source of truth, including the email MCP server itself, so adding or
|
|
removing an email tool fails loudly everywhere it matters instead of silently
|
|
shrinking one surface.
|
|
"""
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import src.agent_tools # noqa: F401 — resolve the circular-import cluster first
|
|
from src.tool_security import (
|
|
BUILTIN_EMAIL_TOOLS,
|
|
NON_ADMIN_BLOCKED_TOOLS,
|
|
PLAN_MODE_READONLY_TOOLS,
|
|
_PLAN_MODE_KNOWN_MUTATORS,
|
|
)
|
|
|
|
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
|
|
# Read-only email tools — the only ones plan mode may execute.
|
|
_READONLY_EMAIL = {"list_email_accounts", "list_emails", "read_email", "search_emails"}
|
|
|
|
|
|
def test_email_server_tools_match_builtin_set():
|
|
"""BUILTIN_EMAIL_TOOLS must equal exactly what the email server exposes."""
|
|
source = (_REPO_ROOT / "mcp_servers" / "email_server.py").read_text()
|
|
served = set(re.findall(r'Tool\(\s*name="(\w+)"', source))
|
|
assert served == set(BUILTIN_EMAIL_TOOLS), (
|
|
f"email_server tools != BUILTIN_EMAIL_TOOLS; "
|
|
f"server-only: {sorted(served - BUILTIN_EMAIL_TOOLS)}, "
|
|
f"set-only: {sorted(BUILTIN_EMAIL_TOOLS - served)}"
|
|
)
|
|
|
|
|
|
def test_fence_tags_cover_email_tools():
|
|
from src.agent_tools import TOOL_TAGS
|
|
|
|
assert BUILTIN_EMAIL_TOOLS <= set(TOOL_TAGS)
|
|
|
|
|
|
def test_function_schemas_cover_email_tools():
|
|
"""Every email tool must be advertised to native function-calling models."""
|
|
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
|
|
|
|
names = {(t.get("function") or {}).get("name") for t in FUNCTION_TOOL_SCHEMAS}
|
|
missing = set(BUILTIN_EMAIL_TOOLS) - names
|
|
assert not missing, f"email tools without native schemas: {sorted(missing)}"
|
|
|
|
|
|
def test_prompt_sections_cover_email_tools():
|
|
"""Every email tool must be describable to fenced-block models."""
|
|
from src.agent_loop import TOOL_SECTIONS
|
|
|
|
missing = set(BUILTIN_EMAIL_TOOLS) - set(TOOL_SECTIONS)
|
|
assert not missing, f"email tools without prompt sections: {sorted(missing)}"
|
|
|
|
|
|
def test_domain_rules_map_covers_email_tools():
|
|
from src.agent_loop import _DOMAIN_TOOL_MAP
|
|
|
|
assert BUILTIN_EMAIL_TOOLS <= _DOMAIN_TOOL_MAP["email"]
|
|
|
|
|
|
def test_rag_descriptions_and_keyword_hints_cover_email_tools():
|
|
"""Every email tool must be retrievable (embedding index + keyword hints)."""
|
|
from src.tool_index import (
|
|
ASSISTANT_ALWAYS_AVAILABLE,
|
|
BUILTIN_TOOL_DESCRIPTIONS,
|
|
)
|
|
|
|
missing = set(BUILTIN_EMAIL_TOOLS) - set(BUILTIN_TOOL_DESCRIPTIONS)
|
|
assert not missing, f"email tools not in the RAG description index: {sorted(missing)}"
|
|
assert BUILTIN_EMAIL_TOOLS <= ASSISTANT_ALWAYS_AVAILABLE
|
|
|
|
|
|
def test_known_tool_names_cover_email_tools():
|
|
from src.tool_policy import _COMMON_TOOL_NAMES
|
|
|
|
assert BUILTIN_EMAIL_TOOLS <= _COMMON_TOOL_NAMES
|
|
|
|
|
|
def test_non_admin_blocklist_covers_email_tools():
|
|
assert BUILTIN_EMAIL_TOOLS <= NON_ADMIN_BLOCKED_TOOLS
|
|
|
|
|
|
def test_plan_mode_partitions_email_tools():
|
|
"""Plan mode must classify every email tool: the read-only four are
|
|
allowed (both spellings, given the bare/qualified alias gate), everything
|
|
else is in the fail-closed mutator backstop. An unclassified tool would be
|
|
allowed bare and blocked qualified — the round-2 aliasing bug again."""
|
|
readonly = set(BUILTIN_EMAIL_TOOLS) & PLAN_MODE_READONLY_TOOLS
|
|
assert readonly == _READONLY_EMAIL, (
|
|
f"plan-mode read-only email subset drifted: {sorted(readonly)}"
|
|
)
|
|
mutators = set(BUILTIN_EMAIL_TOOLS) - _READONLY_EMAIL
|
|
missing = mutators - _PLAN_MODE_KNOWN_MUTATORS
|
|
assert not missing, f"mutating email tools not in plan-mode backstop: {sorted(missing)}"
|
|
|
|
|
|
def test_frontend_tool_selector_covers_email_tools():
|
|
"""The assistant tool-selector UI must be able to grant/revoke every email
|
|
tool — a name missing here can never be toggled from the UI."""
|
|
source = (_REPO_ROOT / "static" / "js" / "assistant.js").read_text()
|
|
m = re.search(r"'Email':\s*\[([^\]]*)\]", source)
|
|
assert m, "Email group not found in assistant.js TOOL_GROUPS"
|
|
listed = set(re.findall(r"'(\w+)'", m.group(1)))
|
|
assert listed == set(BUILTIN_EMAIL_TOOLS), (
|
|
f"assistant.js Email group != BUILTIN_EMAIL_TOOLS; "
|
|
f"js-only: {sorted(listed - BUILTIN_EMAIL_TOOLS)}, "
|
|
f"missing: {sorted(BUILTIN_EMAIL_TOOLS - listed)}"
|
|
)
|
|
|
|
|
|
def test_default_assistant_seed_covers_email_tools():
|
|
"""The default Assistant crew seed grants the full email set."""
|
|
source = (_REPO_ROOT / "src" / "task_scheduler.py").read_text()
|
|
assert "*sorted(BUILTIN_EMAIL_TOOLS)" in source, (
|
|
"default assistant enabled_tools no longer derives from BUILTIN_EMAIL_TOOLS"
|
|
)
|