diff --git a/gateway/run.py b/gateway/run.py index 30e6fc349..6737886a2 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -253,43 +253,54 @@ class GatewayRunner: Check if a user is authorized to use the bot. Checks in order: - 1. Environment variable allowlists (TELEGRAM_ALLOWED_USERS, etc.) - 2. DM pairing approved list - 3. If no allowlists AND no pairing approvals exist, allow all (open access) + 1. Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true) + 2. Environment variable allowlists (TELEGRAM_ALLOWED_USERS, etc.) + 3. DM pairing approved list + 4. Global allow-all (GATEWAY_ALLOW_ALL_USERS=true) + 5. Default: deny """ user_id = source.user_id if not user_id: - return False # Can't verify unknown users - - # Check platform-specific allowlist first + return False + platform_env_map = { Platform.TELEGRAM: "TELEGRAM_ALLOWED_USERS", Platform.DISCORD: "DISCORD_ALLOWED_USERS", Platform.WHATSAPP: "WHATSAPP_ALLOWED_USERS", Platform.SLACK: "SLACK_ALLOWED_USERS", } - - platform_allowlist = os.getenv(platform_env_map.get(source.platform, "")) - global_allowlist = os.getenv("GATEWAY_ALLOWED_USERS", "") - + platform_allow_all_map = { + Platform.TELEGRAM: "TELEGRAM_ALLOW_ALL_USERS", + Platform.DISCORD: "DISCORD_ALLOW_ALL_USERS", + Platform.WHATSAPP: "WHATSAPP_ALLOW_ALL_USERS", + Platform.SLACK: "SLACK_ALLOW_ALL_USERS", + } + + # Per-platform allow-all flag (e.g., DISCORD_ALLOW_ALL_USERS=true) + platform_allow_all_var = platform_allow_all_map.get(source.platform, "") + if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in ("true", "1", "yes"): + return True + # Check pairing store (always checked, regardless of allowlists) platform_name = source.platform.value if source.platform else "" if self.pairing_store.is_approved(platform_name, user_id): return True - - # If no allowlists configured: default-deny unless explicitly opted in. - # Set GATEWAY_ALLOW_ALL_USERS=true in ~/.hermes/.env for open access. + + # Check platform-specific and global allowlists + platform_allowlist = os.getenv(platform_env_map.get(source.platform, ""), "").strip() + global_allowlist = os.getenv("GATEWAY_ALLOWED_USERS", "").strip() + if not platform_allowlist and not global_allowlist: - allow_all = os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in ("true", "1", "yes") - return allow_all - + # No allowlists configured -- check global allow-all flag + return os.getenv("GATEWAY_ALLOW_ALL_USERS", "").lower() in ("true", "1", "yes") + # Check if user is in any allowlist allowed_ids = set() if platform_allowlist: - allowed_ids.update(uid.strip() for uid in platform_allowlist.split(",")) + allowed_ids.update(uid.strip() for uid in platform_allowlist.split(",") if uid.strip()) if global_allowlist: - allowed_ids.update(uid.strip() for uid in global_allowlist.split(",")) - + allowed_ids.update(uid.strip() for uid in global_allowlist.split(",") if uid.strip()) + return user_id in allowed_ids async def _handle_message(self, event: MessageEvent) -> Optional[str]: diff --git a/tools/file_tools.py b/tools/file_tools.py index 4855552d0..c9708b2eb 100644 --- a/tools/file_tools.py +++ b/tools/file_tools.py @@ -199,7 +199,7 @@ def _check_file_reqs(): READ_FILE_SCHEMA = { "name": "read_file", - "description": "Read a file with line numbers and pagination. Output format: 'LINE_NUM|CONTENT'. Suggests similar filenames if not found. Images (png/jpg/gif/webp) returned as base64. Use offset and limit for large files.", + "description": "Read a file with line numbers and pagination. Use this instead of cat/head/tail in terminal. Output format: 'LINE_NUM|CONTENT'. Suggests similar filenames if not found. Images (png/jpg/gif/webp) returned as base64. Use offset and limit for large files.", "parameters": { "type": "object", "properties": { @@ -213,7 +213,7 @@ READ_FILE_SCHEMA = { WRITE_FILE_SCHEMA = { "name": "write_file", - "description": "Write content to a file, completely replacing existing content. Creates parent directories automatically. OVERWRITES the entire file — use 'patch' for targeted edits.", + "description": "Write content to a file, completely replacing existing content. Use this instead of echo/cat heredoc in terminal. Creates parent directories automatically. OVERWRITES the entire file — use 'patch' for targeted edits.", "parameters": { "type": "object", "properties": { @@ -226,7 +226,7 @@ WRITE_FILE_SCHEMA = { PATCH_SCHEMA = { "name": "patch", - "description": "Targeted find-and-replace edits in files. Uses fuzzy matching (9 strategies) so minor whitespace/indentation differences won't break it. Returns a unified diff. Auto-runs syntax checks after editing.\n\nReplace mode (default): find a unique string and replace it.\nPatch mode: apply V4A multi-file patches for bulk changes.", + "description": "Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal. Uses fuzzy matching (9 strategies) so minor whitespace/indentation differences won't break it. Returns a unified diff. Auto-runs syntax checks after editing.\n\nReplace mode (default): find a unique string and replace it.\nPatch mode: apply V4A multi-file patches for bulk changes.", "parameters": { "type": "object", "properties": { @@ -243,7 +243,7 @@ PATCH_SCHEMA = { SEARCH_FILES_SCHEMA = { "name": "search_files", - "description": "Search file contents or find files by name. Ripgrep-backed, faster than grep/rg/find in the terminal.\n\nContent search (target='content'): Regex search inside files. Output modes: full matches with line numbers, file paths only, or match counts.\n\nFile search (target='files'): Find files by glob pattern (e.g., '*.py', '*config*'). Results sorted by modification time.", + "description": "Search file contents or find files by name. Use this instead of grep/rg/find/ls in terminal. Ripgrep-backed, faster than shell equivalents.\n\nContent search (target='content'): Regex search inside files. Output modes: full matches with line numbers, file paths only, or match counts.\n\nFile search (target='files'): Find files by glob pattern (e.g., '*.py', '*config*'). Also use this instead of ls — results sorted by modification time.", "parameters": { "type": "object", "properties": { diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 09d1ff31d..344db7c89 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -348,7 +348,14 @@ from tools.environments.modal import ModalEnvironment as _ModalEnvironment # Tool description for LLM -TERMINAL_TOOL_DESCRIPTION = """Execute commands on a Linux environment. Filesystem persists between calls. +TERMINAL_TOOL_DESCRIPTION = """Execute shell commands on a Linux environment. Filesystem persists between calls. + +Do NOT use cat/head/tail to read files — use read_file instead. +Do NOT use grep/rg/find to search — use search_files instead. +Do NOT use ls to list directories — use search_files(target='files') instead. +Do NOT use sed/awk to edit files — use patch instead. +Do NOT use echo/cat heredoc to create files — use write_file instead. +Reserve terminal for: builds, installs, git, processes, scripts, network, package managers, and anything that needs a shell. Background processes: Set background=true to get a session_id, then use the 'process' tool to poll/wait/kill/write. Working directory: Use 'workdir' for per-command cwd.