fix(security): block all bare email tool names for non-admins; harden fence-tag regex

Review follow-up on #3681 (thanks @vgalin):

1. Routing bare email names made 10 of the 14 email tools executable by
   non-admin owners — is_public_blocked_tool() runs on the bare name
   before dispatch, and NON_ADMIN_BLOCKED_TOOLS only listed 4. Define the
   full email tool set once (BUILTIN_EMAIL_TOOLS in tool_security.py) and
   derive the blocklist, the fence tags (TOOL_TAGS), the bare-name
   dispatch, and the native-call mapping from it so they can't drift.
   This also fixes 4 tools (search_emails, draft_email, draft_email_reply,
   ai_draft_email_reply) that were missing from the old tool_schemas copy
   and therefore unreachable even for native function-calling models.

2. The relaxed fence regex from the previous commit could prefix-match
   longer fence tags: ```python3 parsed as tool "python" with content
   "3\nprint(...)" and executed as code. Add a (?![\w-]) boundary after
   the tag.

Tests: test_public_agent_policy_blocks_sensitive_tools now covers all 14
bare email names + the mcp__email__ form; new tests/test_fenced_inline_args.py
pins inline-args parsing, the python3/hyphenated-tag non-matches, and
strip/parse display mirroring.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
botinate
2026-06-11 01:27:54 -07:00
parent f24dbadab3
commit 544a664766
7 changed files with 128 additions and 22 deletions
+10 -1
View File
@@ -616,7 +616,16 @@ async def test_public_agent_policy_blocks_sensitive_tools(monkeypatch):
monkeypatch.setattr(auth_mod, "AuthManager", lambda: FakeAuth())
for tool_name in ("send_email", "read_file", "mcp__email__send_email"):
# Every bare email tool name is spelled out (not imported from
# BUILTIN_EMAIL_TOOLS) so accidentally dropping one from that set fails
# here instead of silently shrinking the blocklist.
bare_email_tools = (
"list_email_accounts", "list_emails", "read_email", "search_emails",
"send_email", "reply_to_email", "draft_email", "draft_email_reply",
"ai_draft_email_reply", "archive_email", "delete_email",
"mark_email_read", "bulk_email", "download_attachment",
)
for tool_name in bare_email_tools + ("read_file", "mcp__email__send_email"):
desc, result = await execute_tool_block(
SimpleNamespace(tool_type=tool_name, content="{}"),
owner="regular-user",