Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
H
hermes-node-gateway
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
lisa
hermes-node-gateway
Commits
f3df7530
Commit
f3df7530
authored
May 17, 2026
by
Lisa (Hermes AI)
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: auto-proxy sibling Hermes sessions to primary gateway
parent
2981f72e
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
279 additions
and
41 deletions
+279
-41
__init__.py
__init__.py
+279
-41
No files found.
__init__.py
View file @
f3df7530
...
...
@@ -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"
,
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment