Commit f3df7530 authored by Lisa (Hermes AI)'s avatar Lisa (Hermes AI)

fix: auto-proxy sibling Hermes sessions to primary gateway

parent 2981f72e
......@@ -81,7 +81,7 @@ from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Any
from typing import Dict, List, Optional, Any, Tuple
logger = logging.getLogger(__name__)
......@@ -228,6 +228,12 @@ class HttpProxyGateway:
close = stop
def reload_config_sync(self) -> Dict[str, Any]:
return self._request('POST', '/config/reload')
def _request(self, method: str, path: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
url = f"{self.base_url}{path}"
......@@ -392,7 +398,11 @@ class NodeGateway:
self.cert_dir = Path(config.get('cert_dir', '~/.config/hermes-node-gateway/certs')).expanduser()
self.tokens = config.get('tokens', {})
self.config_path = str(Path(config.get(
'config_path', '~/.config/hermes-node-gateway/config.json'
)).expanduser())
self.tokens = dict(config.get('tokens', {}) or {})
self._config_lock = threading.Lock()
# In-memory state (thread-safe)
......@@ -454,6 +464,86 @@ class NodeGateway:
return {'allow': [], 'ask': [], 'deny': []}
def reload_config(self) -> Dict[str, Any]:
"""Reload node tokens and permissions from disk without restarting Hermes."""
config_path = Path(self.config_path).expanduser()
loaded_config: Dict[str, Any] = {}
try:
with open(config_path) as f:
loaded_config = json.load(f)
except FileNotFoundError:
logger.warning(f"Node gateway config not found during reload: {config_path}")
except Exception as e:
logger.error(f"Error reloading node gateway config from {config_path}: {e}")
raise
new_tokens = dict(loaded_config.get('tokens', {}) or {})
permissions_path = loaded_config.get(
'permissions_path',
self.config.get('permissions_path', '~/.config/hermes-node-gateway/permissions.json')
)
new_permissions = self._load_permissions(permissions_path)
with self._config_lock:
self.config.update(loaded_config)
self.config['config_path'] = str(config_path)
self.config['tokens'] = new_tokens
self.config['permissions_path'] = permissions_path
self.tokens = new_tokens
self.permissions = new_permissions
logger.info(
"Reloaded node gateway config from %s — tokens for %s",
config_path,
sorted(new_tokens.keys()),
)
return {
'success': True,
'config_path': str(config_path),
'token_nodes': sorted(new_tokens.keys()),
'token_count': len(new_tokens),
'permissions_path': str(Path(permissions_path).expanduser()),
}
def start(self):
......@@ -615,6 +705,8 @@ class NodeGateway:
app.router.add_get('/nodes', self.http_list_nodes)
app.router.add_post('/config/reload', self.http_reload_config)
app.router.add_get('/nodes/{node_name}/status', self.http_node_status)
app.router.add_post('/nodes/{node_name}/exec', self.http_exec_command)
......@@ -1945,6 +2037,21 @@ class NodeGateway:
return web.json_response(self.list_nodes())
async def http_reload_config(self, request):
"""HTTP handler: reload token/permission config without restart."""
from aiohttp import web
try:
return web.json_response(self.reload_config())
except Exception as e:
return web.json_response({'success': False, 'error': str(e)}, status=500)
async def http_node_status(self, request):
"""HTTP handler: get one node status."""
......@@ -2495,6 +2602,119 @@ def _build_http_proxy_gateway(config: Dict[str, Any]) -> "HttpProxyGateway":
def _gateway_http_available(config: Dict[str, Any], timeout: float = 1.0) -> bool:
"""Return True when a primary node gateway HTTP API is already reachable."""
url = f"{_gateway_http_base_url(config)}/nodes"
try:
req = urllib_request.Request(url, method='GET')
with urllib_request.urlopen(req, timeout=timeout) as response:
return 200 <= getattr(response, 'status', 200) < 300
except Exception:
return False
def _should_proxy_to_primary_gateway(config: Dict[str, Any]) -> bool:
"""Use proxy mode for child sessions and sibling Hermes processes when the gateway already exists."""
return _running_inside_gateway_session() or _gateway_http_available(config)
def _load_node_gateway_config() -> Tuple[Dict[str, Any], str]:
"""Load Hermes node gateway settings plus external token config."""
try:
from hermes_cli.config import load_config
hermes_config = load_config()
except ImportError:
hermes_config = {}
node_gateway_config = hermes_config.get('node_gateway', {}) or {}
config_path = node_gateway_config.get(
'config_path', '/home/lisa/.config/hermes-node-gateway/config.json'
)
config_path = str(Path(config_path).expanduser())
tokens = {}
permissions_path = node_gateway_config.get(
'permissions_path', '/home/lisa/.config/hermes-node-gateway/permissions.json'
)
try:
with open(config_path) as f:
gateway_config = json.load(f)
tokens = gateway_config.get('tokens', {}) or {}
permissions_path = gateway_config.get('permissions_path', permissions_path)
logger.info(f"Loaded {len(tokens)} node tokens from {config_path}")
except FileNotFoundError:
logger.warning(f"Node gateway config not found: {config_path}")
logger.warning("Create config file with: {'tokens': {'node-name': 'token-value'}}")
except Exception as e:
logger.error(f"Error loading node gateway config: {e}")
return {
'bind_address': node_gateway_config.get('bind_address', '0.0.0.0'),
'websocket_port': node_gateway_config.get('websocket_port', 8765),
'http_port': node_gateway_config.get('http_port', 8766),
'use_tls': node_gateway_config.get('use_tls', True),
'cert_dir': node_gateway_config.get(
'cert_dir', '/home/lisa/.config/hermes-node-gateway/certs'
),
'config_path': config_path,
'tokens': tokens,
'permissions_path': permissions_path,
}, config_path
def _get_gateway() -> NodeGateway:
"""Get the singleton gateway instance"""
......@@ -2517,7 +2737,7 @@ def _init_gateway(config: Dict[str, Any]) -> NodeGateway:
if _gateway is None:
if _running_inside_gateway_session():
if _should_proxy_to_primary_gateway(config):
_gateway = _build_http_proxy_gateway(config)
......@@ -2527,6 +2747,20 @@ def _init_gateway(config: Dict[str, Any]) -> NodeGateway:
_gateway.start()
else:
try:
_gateway.config.update(config)
if hasattr(_gateway, 'config_path'):
_gateway.config_path = config.get('config_path', _gateway.config_path)
except Exception:
logger.debug("Unable to update existing node gateway config metadata", exc_info=True)
return _gateway
......@@ -2552,6 +2786,29 @@ def _shutdown_gateway():
# ---------------------------------------------------------------------------
NODE_GATEWAY_RELOAD_CONFIG_SCHEMA = {
"type": "function",
"function": {
"name": "node_gateway_reload_config",
"description": "Reload Hermes node gateway token and permission config from disk without restarting Hermes",
"parameters": {
"type": "object",
"properties": {},
},
},
}
NODE_LIST_SCHEMA = {
"type": "function",
......@@ -3045,6 +3302,15 @@ AUDIO_CONTROL_SCHEMA = {
# ---------------------------------------------------------------------------
def tool_node_gateway_reload_config(*args, **kwargs) -> Dict[str, Any]:
"""Reload node gateway token and permission config from disk."""
gw = _get_gateway()
_normalize_tool_params(args, kwargs)
if hasattr(gw, 'reload_config_sync'):
return gw.reload_config_sync()
return gw.reload_config()
def tool_node_list(*args, **kwargs) -> Dict[str, Any]:
"""List all connected nodes."""
gw = _get_gateway()
......@@ -3230,50 +3496,22 @@ def register(ctx):
Called by the plugin loader to register tools.
"""
try:
# Load config from Hermes config.yaml
try:
from hermes_cli.config import load_config
hermes_config = load_config()
except ImportError:
# Fallback if hermes_cli not available (testing mode)
hermes_config = {}
# Load tokens from external config file
config_path = hermes_config.get('node_gateway', {}).get('config_path',
'/home/lisa/.config/hermes-node-gateway/config.json')
import json
from pathlib import Path
tokens = {}
try:
with open(Path(config_path).expanduser()) as f:
gateway_config = json.load(f)
tokens = gateway_config.get('tokens', {})
logger.info(f"Loaded {len(tokens)} node tokens from {config_path}")
except FileNotFoundError:
logger.warning(f"Node gateway config not found: {config_path}")
logger.warning("Create config file with: {'tokens': {'node-name': 'token-value'}}")
except Exception as e:
logger.error(f"Error loading node gateway config: {e}")
# Build node gateway config
node_config = {
'bind_address': hermes_config.get('node_gateway', {}).get('bind_address', '0.0.0.0'),
'websocket_port': hermes_config.get('node_gateway', {}).get('websocket_port', 8765),
'http_port': hermes_config.get('node_gateway', {}).get('http_port', 8766),
'use_tls': hermes_config.get('node_gateway', {}).get('use_tls', True),
'cert_dir': hermes_config.get('node_gateway', {}).get('cert_dir',
'/home/lisa/.config/hermes-node-gateway/certs'),
'tokens': tokens,
'permissions_path': hermes_config.get('node_gateway', {}).get('permissions_path',
'/home/lisa/.config/hermes-node-gateway/permissions.json'),
}
node_config, config_path = _load_node_gateway_config()
# Start the gateway
gw = _init_gateway(node_config)
logger.info(f"Node gateway initialized — tokens for {list(gw.tokens.keys())}")
# Register tools
ctx.register_tool(
name="node_gateway_reload_config",
toolset="hermes-node-gateway",
schema=NODE_GATEWAY_RELOAD_CONFIG_SCHEMA,
handler=tool_node_gateway_reload_config,
description="Reload node gateway tokens and permissions without restarting Hermes",
emoji="🔄",
)
ctx.register_tool(
name="node_list",
toolset="hermes-node-gateway",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment