mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-11 18:52:21 +02:00
* Add edit_file tool + file-change diffs
edit_file is an exact old_string -> new_string replacement on a file on disk
(fails if old_string is missing or non-unique unless replace_all); write_file
also returns a unified diff. Diffs render collapsed in the tool bubble
(filename + +adds/-dels, theme colors); the raw JSON command box is hidden.
Security: edit_file is a sensitive filesystem-write tool, treated everywhere
write_file is —
- added to NON_ADMIN_BLOCKED_TOOLS (is_public_blocked_tool / blocked_tools_for_owner),
so on auth-enabled deployments a non-admin cannot run it; execute_tool_block
refuses it for non-admin owners.
- confined by the same path policy as read_file/write_file (allowlist +
sensitive-file deny) via _resolve_tool_path.
Disambiguation in tool descriptions + bash prompt: edit_file/write_file are the
only way to write files (they show a diff) — never edit_document (editor panel)
or a bash heredoc/redirect.
Tests (tests/test_edit_file.py): non-admin block (policy + execution gate),
successful edit, not-found old_string, non-unique old_string (+ replace_all),
and path outside the allowed roots.
Files: src/tool_execution.py, src/agent_loop.py, src/tool_schemas.py,
src/agent_tools.py, src/tool_index.py, static/js/chat.js, static/style.css,
tests/test_edit_file.py.
* Drop redundant import os in write_file closure
os is already imported at module top.
85 lines
2.3 KiB
Python
85 lines
2.3 KiB
Python
"""Server-side tool safety policy."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Optional, Set
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Tools regular/public users must not execute directly. These either expose
|
|
# server/runtime access, sensitive user data, external messaging, persistent
|
|
# state changes, or generic loopback/integration surfaces.
|
|
NON_ADMIN_BLOCKED_TOOLS = {
|
|
"bash",
|
|
"python",
|
|
"read_file",
|
|
"write_file",
|
|
"edit_file",
|
|
"search_chats",
|
|
"manage_memory",
|
|
"manage_skills",
|
|
"manage_tasks",
|
|
"manage_endpoints",
|
|
"manage_mcp",
|
|
"manage_webhooks",
|
|
"manage_tokens",
|
|
"manage_documents",
|
|
"manage_settings",
|
|
"api_call",
|
|
"app_api",
|
|
"send_email",
|
|
"reply_to_email",
|
|
"list_emails",
|
|
"read_email",
|
|
"resolve_contact",
|
|
"manage_contact",
|
|
"manage_calendar",
|
|
"vault_search",
|
|
"vault_get",
|
|
"vault_unlock",
|
|
"download_model",
|
|
"serve_model",
|
|
"serve_preset",
|
|
"stop_served_model",
|
|
"cancel_download",
|
|
"adopt_served_model",
|
|
}
|
|
|
|
|
|
def is_public_blocked_tool(tool_name: Optional[str]) -> bool:
|
|
"""Return True when a non-admin/public user must not execute this tool.
|
|
|
|
This is a security gate, so it fails CLOSED: a malformed non-string tool
|
|
name can't be matched against the blocklist or the ``mcp__`` namespace, so
|
|
it is treated as blocked rather than silently allowed through. ``None`` /
|
|
empty string means there is no tool to gate.
|
|
"""
|
|
if tool_name is None or tool_name == "":
|
|
return False
|
|
if not isinstance(tool_name, str):
|
|
return True
|
|
return tool_name in NON_ADMIN_BLOCKED_TOOLS or tool_name.startswith("mcp__")
|
|
|
|
|
|
def owner_is_admin_or_single_user(owner: Optional[str]) -> bool:
|
|
"""Return True for admins, or when auth is not configured yet."""
|
|
try:
|
|
from core.auth import AuthManager
|
|
|
|
auth = AuthManager()
|
|
if not auth.is_configured:
|
|
return True
|
|
return bool(owner and auth.is_admin(owner))
|
|
except Exception as exc:
|
|
logger.warning("Unable to evaluate owner admin status: %s", exc)
|
|
return False
|
|
|
|
|
|
def blocked_tools_for_owner(owner: Optional[str]) -> Set[str]:
|
|
"""Tools to hide/disable for this owner under public-user policy."""
|
|
if owner_is_admin_or_single_user(owner):
|
|
return set()
|
|
return set(NON_ADMIN_BLOCKED_TOOLS)
|