Files

420 lines
11 KiB
Bash
Executable File

#!/bin/sh /etc/rc.common
# luci-app-hermes — procd init script
USE_PROCD=1
START=99
STOP=10
EXTRA_COMMANDS="setup status_service restart_gateway"
EXTRA_HELP=" setup Install Python and Hermes Agent
status_service Show service status
restart_gateway Restart only Gateway instance (does not affect Web PTY)"
HERMES_BASE="/opt/hermes"
HERMES_VENV="${HERMES_BASE}/venv"
HERMES_DATA="${HERMES_BASE}/data"
HERMES_CONFIG="${HERMES_DATA}/config.yaml"
HERMES_BIN="${HERMES_VENV}/bin/hermes"
PYTHON_BIN="${HERMES_VENV}/bin/python3"
# ── OverlayFS compatibility fix ──
# Docker bind mount (/overlay/upper/opt/docker) makes /opt unwritable
# Solution: bind mount upper layer /opt to merged view /opt
_hermes_fix_opt() {
mkdir -p /opt/hermes/.probe 2>/dev/null && { rmdir /opt/hermes/.probe 2>/dev/null; return 0; }
if [ -d /overlay/upper/opt ]; then
mkdir -p /overlay/upper/opt/hermes 2>/dev/null
mount --bind /overlay/upper/opt /opt 2>/dev/null && return 0
fi
return 1
}
_hermes_fix_opt
# ── Port listening check ──
_ensure_port_free() {
local p="$1"
local i=0 max_wait=3
# Check if port is already in use
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
else
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
fi
# Port is occupied, try to clean up
for hermes_pid in $(pgrep -f "hermes.*gateway" 2>/dev/null); do
kill "$hermes_pid" 2>/dev/null
done
while [ $i -lt $max_wait ]; do
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
else
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
fi
sleep 1
i=$((i + 1))
done
# Last resort: SIGKILL
local port_pid=""
if command -v ss >/dev/null 2>&1; then
port_pid=$(ss -tulnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
else
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
fi
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && sleep 1
return 0
}
port_listening() {
local p="$1"
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${p} "
else
netstat -tulnp 2>/dev/null | grep -q ":${p} "
fi
}
sync_uci_to_yaml() {
# Sync UCI config to /root/.hermes/config.yaml
local port bind api_endpoint api_key model
port=$(uci -q get hermes.main.port || echo "3000")
bind=$(uci -q get hermes.main.bind || echo "lan")
api_endpoint=$(uci -q get hermes.main.api_endpoint || echo "https://api.boos.lat/v1")
api_key=$(uci -q get hermes.main.api_key || echo "")
model=$(uci -q get hermes.main.model || echo "opus4.6")
# Map UCI bind to listen address
local listen_host="127.0.0.1"
case "$bind" in
lan) listen_host="0.0.0.0" ;;
loopback) listen_host="127.0.0.1" ;;
all) listen_host="0.0.0.0" ;;
*) listen_host="0.0.0.0" ;;
esac
# Ensure config directory exists
mkdir -p /root/.hermes 2>/dev/null
mkdir -p "$(dirname "$HERMES_CONFIG")" 2>/dev/null
# Generate token if not set
local token
token=$(uci -q get hermes.main.token || echo "")
if [ -z "$token" ]; then
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || echo "hermes_$(date +%s)")
uci set hermes.main.token="$token"
uci commit hermes 2>/dev/null
fi
# Write config.yaml using shell (no Python dependency at this point)
cat > /root/.hermes/config.yaml << YAMLEOF
# Hermes Agent configuration — auto-generated by luci-app-hermes
# UCI sync: $(date '+%Y-%m-%d %H:%M:%S')
gateway:
port: ${port}
bind: ${listen_host}
auth:
mode: token
token: ${token}
api:
endpoint: "${api_endpoint}"
key: "${api_key}"
model:
primary: "${model}"
tools:
profile: coding
update:
checkOnStart: false
YAMLEOF
# Also sync to HERMES_DATA if different
if [ -n "$HERMES_CONFIG" ] && [ "$HERMES_CONFIG" != "/root/.hermes/config.yaml" ]; then
mkdir -p "$(dirname "$HERMES_CONFIG")" 2>/dev/null
cp /root/.hermes/config.yaml "$HERMES_CONFIG" 2>/dev/null || true
fi
chmod 600 /root/.hermes/config.yaml 2>/dev/null || true
}
start_service() {
local enabled port bind pty_port api_endpoint api_key model
config_load hermes
config_get enabled main enabled "0"
config_get port main port "3000"
config_get bind main bind "lan"
config_get pty_port main pty_port "3001"
config_get api_endpoint main api_endpoint "https://api.boos.lat/v1"
config_get api_key main api_key ""
config_get model main model "opus4.6"
[ "$enabled" = "1" ] || {
echo "hermes is disabled. Set enabled to 1 in /etc/config/hermes"
return 0
}
# Check Python
if [ ! -x "$PYTHON_BIN" ]; then
echo "Python not found: $PYTHON_BIN"
echo "Please run: hermes-env setup"
return 1
fi
# Check hermes binary
if [ ! -x "$HERMES_BIN" ]; then
echo "Hermes not installed. Please run: hermes-env setup"
return 1
fi
# Sync UCI to config.yaml
sync_uci_to_yaml
# Ensure gateway port is not occupied by stale processes
_ensure_port_free "$port"
# Calculate memory limit based on system RAM
local mem_limit=""
local total_kb
total_kb=$(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null || echo "0")
if [ "$total_kb" -ge 3000000 ] 2>/dev/null; then
mem_limit="512m"
elif [ "$total_kb" -ge 1500000 ] 2>/dev/null; then
mem_limit="256m"
elif [ "$total_kb" -ge 700000 ] 2>/dev/null; then
mem_limit="128m"
else
mem_limit="64m"
fi
# Start Hermes Gateway (main service, foreground)
procd_open_instance "gateway"
procd_set_param command "$HERMES_BIN" gateway run \
--port "$port" --bind "$bind"
procd_set_param env \
HOME="/root" \
HERMES_HOME="/root/.hermes" \
HERMES_CONFIG="/root/.hermes/config.yaml" \
PATH="${HERMES_VENV}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
PYTHONPATH="${HERMES_VENV}/lib/python3*/site-packages" \
HERMES_API_ENDPOINT="$api_endpoint" \
HERMES_API_KEY="$api_key" \
HERMES_MODEL="$model"
procd_set_param respawn 3600 10 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param pidfile /var/run/hermes.pid
procd_close_instance
# Start Web PTY config terminal (auxiliary service)
local pty_token
pty_token=$(uci -q get hermes.main.pty_token || echo "")
if [ -z "$pty_token" ]; then
pty_token=$(head -c 16 /dev/urandom | hexdump -e '16/1 "%02x"' 2>/dev/null || openssl rand -hex 16 2>/dev/null || echo "pty_$(date +%s)")
uci set hermes.main.pty_token="$pty_token"
uci commit hermes 2>/dev/null
fi
procd_open_instance "pty"
procd_set_param command "$PYTHON_BIN" /usr/share/hermes/web-pty.py
procd_set_param env \
HERMES_PTY_PORT="$pty_port" \
HERMES_PTY_TOKEN="$pty_token" \
HERMES_VENV="$HERMES_VENV" \
HERMES_DATA="$HERMES_DATA" \
HOME="/root" \
PATH="${HERMES_VENV}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_set_param pidfile /var/run/hermes-pty.pid
procd_close_instance
}
_ensure_port_free() {
local p="$1"
local i=0 max_wait=3
# Check if port is already in use
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
else
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
fi
# Port is occupied, try to clean up
for hermes_pid in $(pgrep -f "hermes.*gateway" 2>/dev/null); do
kill "$hermes_pid" 2>/dev/null
done
while [ $i -lt $max_wait ]; do
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
else
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
fi
sleep 1
i=$((i + 1))
done
# Last resort: SIGKILL
local port_pid=""
if command -v ss >/dev/null 2>&1; then
port_pid=$(ss -tulnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
else
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
fi
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && sleep 1
return 0
}
stop_service() {
local port
port=$(uci -q get hermes.main.port || echo "3000")
# Get all related PIDs
local gw_pids=""
gw_pids=$(pgrep -f "hermes.*gateway" 2>/dev/null)
gw_pids="$gw_pids $(pgrep -f "hermes.*gateway.*run" 2>/dev/null)"
# Send SIGTERM to all processes
for pid in $gw_pids; do
[ -n "$pid" ] && kill "$pid" 2>/dev/null
done
# Wait for port to be released
local wait_count=0
local max_wait=4
while [ $wait_count -lt $max_wait ]; do
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${port} " || break
else
netstat -tulnp 2>/dev/null | grep -q ":${port} " || break
fi
sleep 1
wait_count=$((wait_count + 1))
done
# Force kill if still running
if [ $wait_count -ge $max_wait ]; then
local port_pid=""
if command -v ss >/dev/null 2>&1; then
port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
else
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
fi
if [ -n "$port_pid" ]; then
kill -9 "$port_pid" 2>/dev/null
sleep 1
fi
fi
}
service_triggers() {
procd_add_reload_trigger "hermes"
}
reload_service() {
stop
sleep 1
start
}
setup() {
echo "Calling hermes-env setup..."
/usr/bin/hermes-env setup
}
restart_gateway() {
# Restart only the Gateway process, does not affect PTY
local port
port=$(uci -q get hermes.main.port || echo "3000")
# Get gateway PID
local port_pid="" gw_pid=""
if command -v ss >/dev/null 2>&1; then
port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
else
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
fi
gw_pid=$(ubus call service list '{"name":"hermes"}' 2>/dev/null | \
jsonfilter -e '$.hermes.instances.gateway.pid' 2>/dev/null) || true
# Send SIGTERM
[ -n "$port_pid" ] && kill "$port_pid" 2>/dev/null
[ -n "$gw_pid" ] && kill -0 "$gw_pid" 2>/dev/null && kill "$gw_pid" 2>/dev/null
# Kill any remaining hermes gateway processes
for pid in $(pgrep -f "hermes.*gateway" 2>/dev/null) $(pgrep -f "hermes.*gateway.*run" 2>/dev/null); do
kill "$pid" 2>/dev/null
done
# Wait for port to be released
local wait_count=0
while [ $wait_count -lt 3 ]; do
if command -v ss >/dev/null 2>&1; then
ss -tulnp 2>/dev/null | grep -q ":${port} " || break
else
netstat -tulnp 2>/dev/null | grep -q ":${port} " || break
fi
sleep 1
wait_count=$((wait_count + 1))
done
# Force kill if still running
if [ $wait_count -ge 3 ]; then
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null
sleep 1
fi
# If no procd gateway, restart the service
if [ -z "$gw_pid" ]; then
/etc/init.d/hermes start >/dev/null 2>&1
fi
}
status_service() {
local port pty_port
port=$(uci -q get hermes.main.port || echo "3000")
pty_port=$(uci -q get hermes.main.pty_port || echo "3001")
echo "=== Hermes Agent Service Status ==="
# Python
if [ -x "$PYTHON_BIN" ]; then
echo "Python: $($PYTHON_BIN --version 2>/dev/null)"
else
echo "Python: Not installed"
fi
# Hermes Agent
if [ -x "$HERMES_BIN" ]; then
echo "Hermes: $($HERMES_BIN --version 2>/dev/null | head -1)"
else
echo "Hermes: Not installed"
fi
# Gateway
if port_listening "$port"; then
echo "Gateway: ✅ Running (:$port)"
else
echo "Gateway: ⏹ Stopped"
fi
# PTY
if port_listening "$pty_port"; then
echo "Config PTY: ✅ Running (:$pty_port)"
else
echo "Config PTY: ⏹ Stopped"
fi
# Config
echo "Config: $HERMES_CONFIG"
echo "Data Dir: $HERMES_DATA"
}