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
3c675690
Commit
3c675690
authored
May 13, 2026
by
Lisa (Hermes AI)
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
license: switch gateway plugin sources to GPLv3 headers
parent
00044737
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
353 additions
and
20 deletions
+353
-20
CHANGELOG.md
CHANGELOG.md
+3
-0
INTEGRATION_COMPLETE.md
INTEGRATION_COMPLETE.md
+3
-0
LICENSE
LICENSE
+10
-17
README.md
README.md
+3
-0
USER_SPACE_SETUP.md
USER_SPACE_SETUP.md
+3
-0
__init__.py
__init__.py
+325
-3
build.sh
build.sh
+6
-0
No files found.
CHANGELOG.md
View file @
3c675690
<!-- Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net> -->
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
<!-- Copyleft: GNU GPLv3 or later applies to this file. -->
# Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved.
...
...
INTEGRATION_COMPLETE.md
View file @
3c675690
<!-- Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net> -->
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
<!-- Copyleft: GNU GPLv3 or later applies to this file. -->
# Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved.
...
...
LICENSE
View file @
3c675690
MIT License
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (c) 2026 Lisa (Hermes AI) / OpenClaw Project
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
Th
e above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software
.
Th
is project is licensed under the GNU General Public License version 3
or (at your option) any later version
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>.
README.md
View file @
3c675690
<!-- Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net> -->
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
<!-- Copyleft: GNU GPLv3 or later applies to this file. -->
# Hermes Node Gateway Plugin
**Repository:**
`git@git.nexlab.net:lisa/hermes-node-gateway.git`
...
...
USER_SPACE_SETUP.md
View file @
3c675690
<!-- Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net> -->
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
<!-- Copyleft: GNU GPLv3 or later applies to this file. -->
# Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved.
...
...
__init__.py
View file @
3c675690
# Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyleft: this program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
...
...
@@ -433,6 +439,8 @@ class NodeGateway:
app
.
router
.
add_post
(
'/nodes/{node_name}/audio'
,
self
.
http_audio_control
)
app
.
router
.
add_post
(
'/nodes/{node_name}/camera'
,
self
.
http_camera_control
)
self
.
_http_runner
=
web
.
AppRunner
(
app
)
await
self
.
_http_runner
.
setup
()
...
...
@@ -716,6 +724,18 @@ class NodeGateway:
await
self
.
_handle_cc_result
(
msg
)
elif
msg_type
==
'desktop_observe_result'
:
await
self
.
_handle_desktop_observe_result
(
msg
)
elif
msg_type
==
'audio_control_result'
:
await
self
.
_handle_audio_control_result
(
msg
)
elif
msg_type
==
'camera_control_result'
:
await
self
.
_handle_camera_control_result
(
msg
)
except
Exception
as
e
:
...
...
@@ -906,6 +926,46 @@ class NodeGateway:
self
.
command_waiters
[
cmd_id
]
.
set_result
(
cmd
)
async
def
_handle_camera_control_result
(
self
,
msg
:
dict
):
"""Handle camera_control response from node"""
cmd_id
=
msg
.
get
(
"id"
)
if
cmd_id
not
in
self
.
command_waiters
:
logger
.
warning
(
f
"Response for unknown camera_control command: {cmd_id}"
)
return
with
self
.
_commands_lock
:
if
cmd_id
not
in
self
.
commands
:
return
cmd
=
self
.
commands
[
cmd_id
]
success
=
msg
.
get
(
"success"
,
False
)
cmd
.
status
=
"completed"
if
success
else
"failed"
cmd
.
exit_code
=
0
if
success
else
-
1
cmd
.
error
=
msg
.
get
(
"error"
)
cmd
.
completed_at
=
time
.
time
()
cmd
.
browser_result
=
msg
logger
.
info
(
f
"camera_control command {cmd_id} completed: {cmd.status}"
)
if
cmd_id
in
self
.
command_waiters
:
self
.
command_waiters
[
cmd_id
]
.
set_result
(
cmd
)
async
def
execute_browser_command
(
self
,
...
...
@@ -1498,10 +1558,180 @@ class NodeGateway:
tools
.
append
(
'audio_control'
)
if
isinstance
(
caps
,
dict
)
and
caps
.
get
(
'enable_camera_control'
):
tools
.
append
(
'camera_control'
)
return
tools
def
execute_camera_control_command_sync
(
self
,
node_name
:
str
,
command
:
Dict
[
str
,
Any
],
timeout
:
int
=
30
)
->
Dict
[
str
,
Any
]:
"""Synchronous wrapper for execute_camera_control_command"""
if
not
self
.
_loop
:
raise
RuntimeError
(
"Node gateway event loop is not running"
)
future
=
asyncio
.
run_coroutine_threadsafe
(
self
.
execute_camera_control_command
(
node_name
,
command
,
timeout
),
self
.
_loop
)
return
future
.
result
(
timeout
=
timeout
+
5
)
async
def
execute_camera_control_command
(
self
,
node_name
:
str
,
command
:
Dict
[
str
,
Any
],
timeout
:
int
=
30
)
->
Dict
[
str
,
Any
]:
"""Execute a camera_control command on a node (async)"""
with
self
.
_nodes_lock
:
if
node_name
not
in
self
.
nodes
:
raise
ValueError
(
f
"Node '{node_name}' is not connected"
)
node
=
self
.
nodes
[
node_name
]
tools
=
self
.
_get_node_tools
(
node
)
if
"camera_control"
not
in
tools
:
raise
ValueError
(
f
"Node '{node_name}' does not support camera_control"
)
cmd_id
=
f
"camera-{uuid.uuid4().hex[:8]}"
cmd
=
CommandExecution
(
id
=
cmd_id
,
node_name
=
node_name
,
command
=
command
,
status
=
'pending'
,
approved
=
True
,
started_at
=
time
.
time
()
)
with
self
.
_commands_lock
:
self
.
commands
[
cmd_id
]
=
cmd
msg
=
{
"type"
:
"camera_control"
,
"id"
:
cmd_id
,
**
command
}
await
node
.
socket
.
send
(
json
.
dumps
(
msg
))
cmd
.
status
=
'running'
logger
.
info
(
f
"Sent camera_control command to node '{node_name}': {command.get('action')}"
)
future
=
asyncio
.
Future
()
self
.
command_waiters
[
cmd_id
]
=
future
try
:
await
asyncio
.
wait_for
(
future
,
timeout
=
timeout
)
except
asyncio
.
TimeoutError
:
with
self
.
_commands_lock
:
if
cmd_id
in
self
.
commands
:
self
.
commands
[
cmd_id
]
.
status
=
'failed'
self
.
commands
[
cmd_id
]
.
error
=
f
'Command timed out after {timeout}s'
self
.
commands
[
cmd_id
]
.
completed_at
=
time
.
time
()
self
.
command_waiters
.
pop
(
cmd_id
,
None
)
raise
TimeoutError
(
f
"camera_control command timed out after {timeout}s"
)
finally
:
self
.
command_waiters
.
pop
(
cmd_id
,
None
)
with
self
.
_commands_lock
:
cmd
=
self
.
commands
[
cmd_id
]
if
cmd
.
browser_result
is
not
None
:
return
cmd
.
browser_result
return
{
'success'
:
cmd
.
status
==
'completed'
,
'error'
:
cmd
.
error
,
'status'
:
cmd
.
status
,
}
async
def
http_camera_control
(
self
,
request
):
"""HTTP handler: camera control on node."""
from
aiohttp
import
web
node_name
=
request
.
match_info
[
'node_name'
]
try
:
payload
=
await
request
.
json
()
except
Exception
:
return
web
.
json_response
({
'error'
:
'Invalid JSON body'
},
status
=
400
)
timeout
=
payload
.
get
(
'timeout'
,
30
)
try
:
result
=
await
self
.
execute_camera_control_command
(
node_name
,
payload
,
timeout
)
return
web
.
json_response
(
result
)
except
ValueError
as
e
:
return
web
.
json_response
({
'error'
:
str
(
e
)},
status
=
404
)
except
Exception
as
e
:
return
web
.
json_response
({
'error'
:
str
(
e
)},
status
=
500
)
async
def
http_list_nodes
(
self
,
request
):
"""HTTP handler: list connected nodes."""
...
...
@@ -1912,12 +2142,12 @@ class NodeGateway:
logger
.
info
(
f
"Sent command {cmd_id} to node '{node_name}': {cmd_str}"
)
# Wait for completion
# Register waiter before yielding control so fast node replies can't race us
future
=
asyncio
.
Future
()
self
.
command_waiters
[
cmd_id
]
=
future
# Wait for completion
try
:
...
...
@@ -2403,6 +2633,71 @@ DESKTOP_OBSERVE_SCHEMA = {
}
CAMERA_CONTROL_SCHEMA
=
{
"type"
:
"function"
,
"function"
:
{
"name"
:
"camera_control"
,
"description"
:
"Control cameras on a remote node via V4L2/ffmpeg. Supports device listing, status, frame capture, and short video capture."
,
"parameters"
:
{
"type"
:
"object"
,
"properties"
:
{
"node_name"
:
{
"type"
:
"string"
,
"description"
:
"Name of the target node with camera_control capability"
},
"action"
:
{
"type"
:
"string"
,
"enum"
:
[
"list_cameras"
,
"get_camera_status"
,
"capture_frame"
,
"capture_video"
],
"description"
:
"Camera action to perform"
},
"params"
:
{
"type"
:
"object"
,
"description"
:
"Action parameters (device, duration, width/height, format, path, etc.)"
,
"additionalProperties"
:
True
},
"timeout"
:
{
"type"
:
"integer"
,
"description"
:
"Timeout in seconds"
,
"default"
:
30
}
},
"required"
:
[
"node_name"
,
"action"
]
}
}
}
AUDIO_CONTROL_SCHEMA
=
{
"type"
:
"function"
,
...
...
@@ -2629,6 +2924,24 @@ def tool_computer_control(*args, **kwargs) -> Dict[str, Any]:
},
timeout
)
def
tool_camera_control
(
*
args
,
**
kwargs
)
->
Dict
[
str
,
Any
]:
"""Execute camera control command on a node."""
gw
=
_get_gateway
()
params
=
_normalize_tool_params
(
args
,
kwargs
)
node_name
=
params
.
get
(
'node_name'
)
or
params
.
get
(
'nodeName'
)
or
params
.
get
(
'node'
)
action
=
params
.
get
(
'action'
)
or
params
.
get
(
'command'
)
cmd_params
=
params
.
get
(
'params'
)
or
params
.
get
(
'arguments'
)
or
{}
timeout
=
params
.
get
(
'timeout'
,
30
)
if
not
node_name
:
raise
ValueError
(
f
"Missing required parameter: 'node_name' (got keys: {sorted(params.keys())})"
)
if
not
action
:
raise
ValueError
(
f
"Missing required parameter: 'action' (got keys: {sorted(params.keys())})"
)
return
gw
.
execute_camera_control_command_sync
(
node_name
,
{
'action'
:
action
,
'params'
:
cmd_params
},
timeout
)
# ---------------------------------------------------------------------------
# Plugin registration
# ---------------------------------------------------------------------------
...
...
@@ -2745,6 +3058,15 @@ def register(ctx):
emoji
=
"🔊"
,
)
ctx
.
register_tool
(
name
=
"camera_control"
,
toolset
=
"hermes-node-gateway"
,
schema
=
CAMERA_CONTROL_SCHEMA
,
handler
=
tool_camera_control
,
description
=
"Camera device control on a remote node"
,
emoji
=
"📷"
,
)
logger
.
info
(
"Hermes Node Gateway plugin registered successfully"
)
...
...
build.sh
View file @
3c675690
#!/bin/bash
# Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyleft: this program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved.
...
...
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