Enhance messaging gateway configuration and security features
- Added new environment variables for Telegram and Discord bot configurations, including `TELEGRAM_ALLOWED_USERS` and `DISCORD_ALLOWED_USERS`, to restrict bot access to specific users. - Updated documentation in AGENTS.md and README.md to include detailed setup instructions for the messaging gateway, emphasizing the importance of user allowlists for security. - Improved the CLI setup wizard to prompt for allowed user IDs during configuration, enhancing user guidance and security awareness. - Refined the gateway run script to support user authorization checks, ensuring only allowed users can interact with the bot.
This commit is contained in:
parent
3e634aa7e4
commit
17a5efb416
9 changed files with 397 additions and 38 deletions
|
|
@ -163,6 +163,44 @@ OPTIONAL_ENV_VARS = {
|
|||
"url": None,
|
||||
"password": True,
|
||||
},
|
||||
# Messaging platform tokens
|
||||
"TELEGRAM_BOT_TOKEN": {
|
||||
"description": "Telegram bot token from @BotFather",
|
||||
"prompt": "Telegram bot token",
|
||||
"url": "https://t.me/BotFather",
|
||||
"password": True,
|
||||
},
|
||||
"TELEGRAM_ALLOWED_USERS": {
|
||||
"description": "Comma-separated Telegram user IDs allowed to use the bot (get ID from @userinfobot)",
|
||||
"prompt": "Allowed Telegram user IDs (comma-separated)",
|
||||
"url": "https://t.me/userinfobot",
|
||||
"password": False,
|
||||
},
|
||||
"DISCORD_BOT_TOKEN": {
|
||||
"description": "Discord bot token from Developer Portal",
|
||||
"prompt": "Discord bot token",
|
||||
"url": "https://discord.com/developers/applications",
|
||||
"password": True,
|
||||
},
|
||||
"DISCORD_ALLOWED_USERS": {
|
||||
"description": "Comma-separated Discord user IDs allowed to use the bot",
|
||||
"prompt": "Allowed Discord user IDs (comma-separated)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
},
|
||||
# Terminal configuration
|
||||
"MESSAGING_CWD": {
|
||||
"description": "Working directory for terminal commands via messaging (Telegram/Discord/etc). CLI always uses current directory.",
|
||||
"prompt": "Messaging working directory (default: home)",
|
||||
"url": None,
|
||||
"password": False,
|
||||
},
|
||||
"SUDO_PASSWORD": {
|
||||
"description": "Sudo password for terminal commands requiring root access",
|
||||
"prompt": "Sudo password",
|
||||
"url": None,
|
||||
"password": True,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Handles: hermes gateway [run|start|stop|restart|status|install|uninstall]
|
|||
|
||||
import asyncio
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
|
@ -13,6 +14,70 @@ from pathlib import Path
|
|||
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Process Management (for manual gateway runs)
|
||||
# =============================================================================
|
||||
|
||||
def find_gateway_pids() -> list:
|
||||
"""Find PIDs of running gateway processes."""
|
||||
pids = []
|
||||
try:
|
||||
# Look for gateway processes with multiple patterns
|
||||
patterns = [
|
||||
"hermes_cli.main gateway",
|
||||
"hermes gateway",
|
||||
"gateway/run.py",
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
["ps", "aux"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
|
||||
for line in result.stdout.split('\n'):
|
||||
# Skip grep and current process
|
||||
if 'grep' in line or str(os.getpid()) in line:
|
||||
continue
|
||||
|
||||
for pattern in patterns:
|
||||
if pattern in line:
|
||||
parts = line.split()
|
||||
if len(parts) > 1:
|
||||
try:
|
||||
pid = int(parts[1])
|
||||
if pid not in pids:
|
||||
pids.append(pid)
|
||||
except ValueError:
|
||||
continue
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return pids
|
||||
|
||||
|
||||
def kill_gateway_processes(force: bool = False) -> int:
|
||||
"""Kill any running gateway processes. Returns count killed."""
|
||||
pids = find_gateway_pids()
|
||||
killed = 0
|
||||
|
||||
for pid in pids:
|
||||
try:
|
||||
if force:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
else:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
killed += 1
|
||||
except ProcessLookupError:
|
||||
# Process already gone
|
||||
pass
|
||||
except PermissionError:
|
||||
print(f"⚠ Permission denied to kill PID {pid}")
|
||||
|
||||
return killed
|
||||
|
||||
|
||||
def is_linux() -> bool:
|
||||
return sys.platform.startswith('linux')
|
||||
|
||||
|
|
@ -343,29 +408,80 @@ def gateway_command(args):
|
|||
sys.exit(1)
|
||||
|
||||
elif subcmd == "stop":
|
||||
if is_linux():
|
||||
systemd_stop()
|
||||
elif is_macos():
|
||||
launchd_stop()
|
||||
else:
|
||||
print("Not supported on this platform.")
|
||||
sys.exit(1)
|
||||
# Try service first, fall back to killing processes directly
|
||||
service_available = False
|
||||
|
||||
if is_linux() and get_systemd_unit_path().exists():
|
||||
try:
|
||||
systemd_stop()
|
||||
service_available = True
|
||||
except subprocess.CalledProcessError:
|
||||
pass # Fall through to process kill
|
||||
elif is_macos() and get_launchd_plist_path().exists():
|
||||
try:
|
||||
launchd_stop()
|
||||
service_available = True
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
if not service_available:
|
||||
# Kill gateway processes directly
|
||||
killed = kill_gateway_processes()
|
||||
if killed:
|
||||
print(f"✓ Stopped {killed} gateway process(es)")
|
||||
else:
|
||||
print("✗ No gateway processes found")
|
||||
|
||||
elif subcmd == "restart":
|
||||
if is_linux():
|
||||
systemd_restart()
|
||||
elif is_macos():
|
||||
launchd_restart()
|
||||
else:
|
||||
print("Not supported on this platform.")
|
||||
sys.exit(1)
|
||||
# Try service first, fall back to killing and restarting
|
||||
service_available = False
|
||||
|
||||
if is_linux() and get_systemd_unit_path().exists():
|
||||
try:
|
||||
systemd_restart()
|
||||
service_available = True
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
elif is_macos() and get_launchd_plist_path().exists():
|
||||
try:
|
||||
launchd_restart()
|
||||
service_available = True
|
||||
except subprocess.CalledProcessError:
|
||||
pass
|
||||
|
||||
if not service_available:
|
||||
# Manual restart: kill existing processes
|
||||
killed = kill_gateway_processes()
|
||||
if killed:
|
||||
print(f"✓ Stopped {killed} gateway process(es)")
|
||||
|
||||
import time
|
||||
time.sleep(2)
|
||||
|
||||
# Start fresh
|
||||
print("Starting gateway...")
|
||||
run_gateway(verbose=False)
|
||||
|
||||
elif subcmd == "status":
|
||||
deep = getattr(args, 'deep', False)
|
||||
if is_linux():
|
||||
|
||||
# Check for service first
|
||||
if is_linux() and get_systemd_unit_path().exists():
|
||||
systemd_status(deep)
|
||||
elif is_macos():
|
||||
elif is_macos() and get_launchd_plist_path().exists():
|
||||
launchd_status(deep)
|
||||
else:
|
||||
print("Not supported on this platform.")
|
||||
sys.exit(1)
|
||||
# Check for manually running processes
|
||||
pids = find_gateway_pids()
|
||||
if pids:
|
||||
print(f"✓ Gateway is running (PID: {', '.join(map(str, pids))})")
|
||||
print(" (Running manually, not as a system service)")
|
||||
print()
|
||||
print("To install as a service:")
|
||||
print(" hermes gateway install")
|
||||
else:
|
||||
print("✗ Gateway is not running")
|
||||
print()
|
||||
print("To start:")
|
||||
print(" hermes gateway # Run in foreground")
|
||||
print(" hermes gateway install # Install as service")
|
||||
|
|
|
|||
|
|
@ -591,6 +591,23 @@ def run_setup_wizard(args):
|
|||
if is_windows:
|
||||
print_info("Note: On Windows, commands run via cmd.exe or PowerShell")
|
||||
|
||||
# Messaging working directory configuration
|
||||
print_info("")
|
||||
print_info("Working Directory for Messaging (Telegram/Discord/etc):")
|
||||
print_info(" The CLI always uses the directory you run 'hermes' from")
|
||||
print_info(" But messaging bots need a static starting directory")
|
||||
|
||||
current_cwd = get_env_value('MESSAGING_CWD') or str(Path.home())
|
||||
print_info(f" Current: {current_cwd}")
|
||||
|
||||
cwd_input = prompt(" Messaging working directory", current_cwd)
|
||||
# Expand ~ to full path
|
||||
if cwd_input.startswith('~'):
|
||||
cwd_expanded = str(Path.home()) + cwd_input[1:]
|
||||
else:
|
||||
cwd_expanded = cwd_input
|
||||
save_env_value("MESSAGING_CWD", cwd_expanded)
|
||||
|
||||
if prompt_yes_no(" Enable sudo support? (allows agent to run sudo commands)", False):
|
||||
print_warning(" SECURITY WARNING: Sudo password will be stored in plaintext")
|
||||
sudo_pass = prompt(" Sudo password (leave empty to skip)", password=True)
|
||||
|
|
@ -720,10 +737,36 @@ def run_setup_wizard(args):
|
|||
save_env_value("TELEGRAM_BOT_TOKEN", token)
|
||||
print_success("Telegram token saved")
|
||||
|
||||
# Allowed users (security)
|
||||
print()
|
||||
print_info("🔒 Security: Restrict who can use your bot")
|
||||
print_info(" To find your Telegram user ID:")
|
||||
print_info(" 1. Message @userinfobot on Telegram")
|
||||
print_info(" 2. It will reply with your numeric ID (e.g., 123456789)")
|
||||
print()
|
||||
allowed_users = prompt("Allowed user IDs (comma-separated, leave empty for open access)")
|
||||
if allowed_users:
|
||||
save_env_value("TELEGRAM_ALLOWED_USERS", allowed_users.replace(" ", ""))
|
||||
print_success("Telegram allowlist configured - only listed users can use the bot")
|
||||
else:
|
||||
print_info("⚠️ No allowlist set - anyone who finds your bot can use it!")
|
||||
|
||||
home_channel = prompt("Home channel ID (optional, for cron delivery)")
|
||||
if home_channel:
|
||||
save_env_value("TELEGRAM_HOME_CHANNEL", home_channel)
|
||||
|
||||
# Check/update existing Telegram allowlist
|
||||
elif existing_telegram:
|
||||
existing_allowlist = get_env_value('TELEGRAM_ALLOWED_USERS')
|
||||
if not existing_allowlist:
|
||||
print_info("⚠️ Telegram has no user allowlist - anyone can use your bot!")
|
||||
if prompt_yes_no("Add allowed users now?", True):
|
||||
print_info(" To find your Telegram user ID: message @userinfobot")
|
||||
allowed_users = prompt("Allowed user IDs (comma-separated)")
|
||||
if allowed_users:
|
||||
save_env_value("TELEGRAM_ALLOWED_USERS", allowed_users.replace(" ", ""))
|
||||
print_success("Telegram allowlist configured")
|
||||
|
||||
# Discord
|
||||
existing_discord = get_env_value('DISCORD_BOT_TOKEN')
|
||||
if existing_discord:
|
||||
|
|
@ -738,10 +781,36 @@ def run_setup_wizard(args):
|
|||
save_env_value("DISCORD_BOT_TOKEN", token)
|
||||
print_success("Discord token saved")
|
||||
|
||||
# Allowed users (security)
|
||||
print()
|
||||
print_info("🔒 Security: Restrict who can use your bot")
|
||||
print_info(" To find your Discord user ID:")
|
||||
print_info(" 1. Enable Developer Mode in Discord settings")
|
||||
print_info(" 2. Right-click your name → Copy ID")
|
||||
print()
|
||||
allowed_users = prompt("Allowed user IDs (comma-separated, leave empty for open access)")
|
||||
if allowed_users:
|
||||
save_env_value("DISCORD_ALLOWED_USERS", allowed_users.replace(" ", ""))
|
||||
print_success("Discord allowlist configured")
|
||||
else:
|
||||
print_info("⚠️ No allowlist set - anyone in servers with your bot can use it!")
|
||||
|
||||
home_channel = prompt("Home channel ID (optional, for cron delivery)")
|
||||
if home_channel:
|
||||
save_env_value("DISCORD_HOME_CHANNEL", home_channel)
|
||||
|
||||
# Check/update existing Discord allowlist
|
||||
elif existing_discord:
|
||||
existing_allowlist = get_env_value('DISCORD_ALLOWED_USERS')
|
||||
if not existing_allowlist:
|
||||
print_info("⚠️ Discord has no user allowlist - anyone can use your bot!")
|
||||
if prompt_yes_no("Add allowed users now?", True):
|
||||
print_info(" To find Discord ID: Enable Developer Mode, right-click name → Copy ID")
|
||||
allowed_users = prompt("Allowed user IDs (comma-separated)")
|
||||
if allowed_users:
|
||||
save_env_value("DISCORD_ALLOWED_USERS", allowed_users.replace(" ", ""))
|
||||
print_success("Discord allowlist configured")
|
||||
|
||||
# =========================================================================
|
||||
# Step 7: Additional Tools (Optional)
|
||||
# =========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue