Commit dc1a4f86 authored by Lisa's avatar Lisa

license: switch node agent bundle sources to GPLv3 headers

parent b8c862e4
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
<!-- 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. -->
# Deploying the Windows Installer # Deploying the Windows Installer
## Quick Deploy ## Quick Deploy
......
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
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 Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
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:
The above copyright notice and this permission notice shall be included in all This project is licensed under the GNU General Public License version 3
copies or substantial portions of the Software. or (at your option) any later version.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR You should have received a copy of the GNU General Public License along
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, with this program. If not, see <https://www.gnu.org/licenses/>.
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.
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
...@@ -204,7 +207,50 @@ All messages are JSON over WebSocket. ...@@ -204,7 +207,50 @@ All messages are JSON over WebSocket.
### Capability Registration ### Capability Registration
Node registers `browser_control` in capabilities during registration: ## Camera Control
**Optional capability** — phase-1 Linux support uses `ffmpeg` + V4L2 (`/dev/video*`). The node only advertises `camera_control` in its `tools` list when `enable_camera_control` is true *and* a usable camera backend/device is present at runtime.
### Actions
- `list_cameras` — enumerate detected camera devices and probe metadata
- `get_camera_status` — current camera backend readiness and discovered devices
- `capture_frame` — grab a single still frame from a selected/default device
- `capture_video` — record a short video clip from a selected/default device
### Gateway → Node: Camera Control
```json
{
"type": "camera_control",
"id": "camera-a1b2c3d4",
"action": "capture_frame",
"params": {
"device": "/dev/video0",
"format": "png",
"width": 1280,
"height": 720
}
}
```
### Node → Gateway: Camera Control Result
```json
{
"type": "camera_control_result",
"id": "camera-a1b2c3d4",
"action": "capture_frame",
"success": true,
"device": "/dev/video0",
"format": "png",
"path": "/tmp/hermes-camera-1714392000.png",
"size_bytes": 123456,
"data_base64": "..."
}
```
Node registers optional tools in its `tools` list during registration and exposes structured readiness metadata under `capabilities`. Existing siblings include `browser_control`, `computer_control`, `desktop_observe`, `audio_control`, and `camera_control`.
```json ```json
{ {
...@@ -230,7 +276,8 @@ Node registers `browser_control` in capabilities during registration: ...@@ -230,7 +276,8 @@ Node registers `browser_control` in capabilities during registration:
"token": "node-sissy-secret-token-abc123", "token": "node-sissy-secret-token-abc123",
"sexec_path": "/home/openclaw/.openclaw/skills/sexec/sexec.sh", "sexec_path": "/home/openclaw/.openclaw/skills/sexec/sexec.sh",
"reconnect_interval": 5, "reconnect_interval": 5,
"heartbeat_interval": 30 "heartbeat_interval": 30,
"enable_camera_control": false
} }
``` ```
......
<!-- 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 Agent # Hermes Node Agent
**Version:** 2.0 **Version:** 2.0
......
<!-- 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. -->
# Windows Installer — Complete Setup Summary # Windows Installer — Complete Setup Summary
## What Was Built ## What Was Built
......
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/usr/bin/env python3 #!/usr/bin/env python3
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/usr/bin/env python3 #!/usr/bin/env python3
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/bin/bash #!/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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
...@@ -40,6 +46,24 @@ tar czf hermes-node-agent-linux.tar.gz hermes-node-agent/ ...@@ -40,6 +46,24 @@ tar czf hermes-node-agent-linux.tar.gz hermes-node-agent/
mv hermes-node-agent-linux.tar.gz "$OLDPWD/dist/" mv hermes-node-agent-linux.tar.gz "$OLDPWD/dist/"
cd "$OLDPWD" cd "$OLDPWD"
# Re-embed the packaged payload into the self-contained Linux installer wrapper
python3 <<'PY'
import base64, pathlib, re
src = pathlib.Path('dist/hermes-node-agent-linux.tar.gz').read_bytes()
b64 = base64.b64encode(src).decode('ascii')
installer_path = pathlib.Path('package-hermes-node-agent/deploy/install-node.sh')
text = installer_path.read_text()
pattern = r"(cat <<'TARBALL_DATA' \| base64 -d \| tar xzf -\n)(.*?)(\nTARBALL_DATA)"
new_text, count = re.subn(pattern, lambda m: m.group(1) + b64 + m.group(3), text, flags=re.S)
if count != 1:
raise SystemExit(f'Expected exactly one TARBALL_DATA block in {installer_path}, found {count}')
pathlib.Path('deploy/linux/install-node.sh').write_text(new_text)
print('✅ Re-embedded packaged payload into deploy/linux/install-node.sh')
PY
# Keep the root installer copy in sync with the packaged source of truth
cp package-hermes-node-agent/install.sh install.sh
# Cleanup # Cleanup
rm -rf "$BUILD_DIR" rm -rf "$BUILD_DIR"
......
#!/bin/bash #!/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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/bin/bash #!/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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
<!-- 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 Browser Agent Chrome Extension # Hermes Browser Agent Chrome Extension
This extension enables browser automation through the Hermes Node Agent. This extension enables browser automation through the Hermes Node Agent.
......
// 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 Browser Agent — native messaging host bridge // Hermes Browser Agent — native messaging host bridge
const NATIVE_HOST = 'hermes.node.agent'; const NATIVE_HOST = 'hermes.node.agent';
const WS_RETRY = 3000; const WS_RETRY = 3000;
......
// 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 Browser Agent Content Script — no-op placeholder // Hermes Browser Agent Content Script — no-op placeholder
// Main functionality lives in background.js routing to native host // Main functionality lives in background.js routing to native host
// This file ensures content context is available when needed // This file ensures content context is available when needed
......
// 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.
const statusEl = document.getElementById('status'); const statusEl = document.getElementById('status');
setTimeout(() => { setTimeout(() => {
chrome.runtime.sendMessage({type:'ping'}, resp => { chrome.runtime.sendMessage({type:'ping'}, resp => {
......
<!-- 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. -->
# Deployment Scripts for Hermes Node Agent # Deployment Scripts for Hermes Node Agent
These scripts build installers for deploying Hermes Node Agent on target machines. These scripts build installers for deploying Hermes Node Agent on target machines.
......
#!/bin/bash #!/bin/bash
# Hermes Node Agent Installer — Linux (self-contained) # 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 Agent Installer — Linux Version
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved.
#
# This software is released under the MIT License with a copyleft clause.
# See the LICENSE file for full terms.
#
# INSTALLER DESCRIPTION:
# Self-contained installer for Linux. Embedded: node agent code, init.d script.
# External: pip install websockets (online required).
# No git clone, no network downloads of our files.
set -e set -e
echo "=== Hermes Node Agent Installer (Linux) ===" echo "=== Hermes Node Agent Installer (Linux) ==="
echo ""
# Check if running as root (for service setup)
if [ "$EUID" -eq 0 ]; then if [ "$EUID" -eq 0 ]; then
RUN_AS_ROOT=true RUN_AS_ROOT=true
else else
RUN_AS_ROOT=false RUN_AS_ROOT=false
echo "WARNING: Not running as root — skipping service install." echo "⚠️ Not running as root — skipping service installation."
echo " To install as a service, run: sudo $0"
echo ""
fi fi
# Check for Python 3
if ! command -v python3 &> /dev/null; then if ! command -v python3 &> /dev/null; then
echo "ERROR: Python 3 required." echo "❌ ERROR: Python 3 is required but not found."
echo " Install Python 3 first, then re-run this installer."
exit 1 exit 1
fi fi
echo "✓ Python: $(python3 --version)" echo "✓ Python: $(python3 --version)"
PIP_CMD="" # Check for pip (will use it to install websockets)
if command -v pip3 &> /dev/null; then PIP_CMD="pip3"; elif command -v pip &> /dev/null; then PIP_CMD="pip"; fi if ! command -v pip3 &> /dev/null && ! command -v pip &> /dev/null; then
if [ -z "$PIP_CMD" ]; then echo "ERROR: pip not found."; exit 1; fi echo "❌ ERROR: pip is required but not found."
echo " Install with: apt install python3-pip (Debian/Ubuntu) or equivalent"
echo "[1/5] Installing websockets..." exit 1
python3 -c "import websockets" 2>/dev/null || $PIP_CMD install --quiet websockets fi
echo "✓ websockets installed" PIP_CMD="pip3"
command -v pip3 &> /dev/null || PIP_CMD="pip"
# Install websockets library (only network call)
echo "[1/5] Installing Python dependencies (websockets)..."
# Check if already installed
if python3 -c "import websockets" 2>/dev/null; then
echo "✓ websockets library already installed"
else
if ! $PIP_CMD install --quiet websockets 2>/dev/null; then
echo "⚠️ Could not install via pip (may need network). Trying with user flag..."
if ! $PIP_CMD install --user --quiet websockets 2>/dev/null; then
echo "❌ Failed to install websockets. Try manually: $PIP_CMD install websockets"
exit 1
fi
fi
echo "✓ websockets library installed"
fi
# Determine install locations
if [ "$RUN_AS_ROOT" = true ]; then if [ "$RUN_AS_ROOT" = true ]; then
AGENT_DIR="/usr/local/bin" AGENT_DIR="/usr/local/bin"
CONFIG_DIR="/etc/hermes-node" CONFIG_DIR="/etc/hermes-node"
USE_SERVICE=true
else else
AGENT_DIR="$HOME/.local/bin" AGENT_DIR="$HOME/.local/bin"
CONFIG_DIR="$HOME/.config/hermes-node" CONFIG_DIR="$HOME/.config/hermes-node"
USE_SERVICE=false
fi fi
mkdir -p "$AGENT_DIR" mkdir -p "$AGENT_DIR"
mkdir -p "$CONFIG_DIR" mkdir -p "$CONFIG_DIR"
echo "[2/5] Extracting embedded files..." # Extract embedded agent files
TMP=$(mktemp -d) echo "[2/5] Extracting embedded agent files..."
cd "$TMP" TMP_EXTRACT=$(mktemp -d)
cat <<'TAR' | base64 -d | tar xzf - ORIG_PWD=$(pwd)
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 cd "$TMP_EXTRACT"
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
TAR' | base64 -d | tar xzf -
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
TAR
echo "✓ Extracted $(ls -1 | wc -l) embedded files" # === EMBEDDED TARBALL (base64) ===
cat <<'TARBALL_DATA' | base64 -d | tar xzf -
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
TARBALL_DATA
cp hermes_node_agent.py "$AGENT_DIR/hermes-node-agent" # Copy agent script
cp hermes-node-agent/hermes_node_agent.py "$AGENT_DIR/hermes-node-agent"
chmod +x "$AGENT_DIR/hermes-node-agent" chmod +x "$AGENT_DIR/hermes-node-agent"
cp browser_controller.py "$AGENT_DIR/" echo "✓ Agent: $AGENT_DIR/hermes-node-agent"
mkdir -p "$AGENT_DIR/hermes_lib"
cp requirements.txt "$AGENT_DIR/hermes_lib/"
cp hermes-node-agent.init.d "$CONFIG_DIR/"
chmod +x "$CONFIG_DIR/hermes-node-agent.init.d"
mkdir -p "$HOME/.config/hermes-node-agent/sexec"
cp sexec.sh "$HOME/.config/hermes-node-agent/sexec/sexec.sh"
chmod +x "$HOME/.config/hermes-node-agent/sexec/sexec.sh"
echo "✓ Installed sexec"
rm -rf "$TMP"
echo "[3/5] Configuring node..." # Copy init.d script (for root installs)
echo if [ "$USE_SERVICE" = true ]; then
cp hermes-node-agent/hermes-node-agent.init.d /etc/init.d/hermes-node-agent
read -p "Gateway host (default: localhost): " GATEWAY_HOST chmod +x /etc/init.d/hermes-node-agent
GATEWAY_HOST=${GATEWAY_HOST:-localhost} echo "✓ Init script: /etc/init.d/hermes-node-agent"
read -p "Gateway port (default: 8765): " GATEWAY_PORT fi
GATEWP=":${GATEWAY_PORT:-8765}"
read -p "Token (leave empty to auto-generate): " NODE_TOKEN # Copy node_gateway.py (for plugin reference, if needed)
if [ -z "$NODE_TOKEN" ]; then cp hermes-node-agent/node_gateway.py "$AGENT_DIR/hermes-node-gateway.py" 2>/dev/null || true
NODE_TOKEN=$(python3 -c "import secrets; print(secrets.token_hex(32))")
echo " Generated token: $NODE_TOKEN" # Run packaged interactive config installer
if [ -f "$TMP_EXTRACT/hermes-node-agent/install.sh" ]; then
chmod +x "$TMP_EXTRACT/hermes-node-agent/install.sh"
mkdir -p "$CONFIG_DIR"
echo "[4/5] Running interactive configuration installer..."
INSTALLER_TMP_ROOT=$(mktemp -d)
trap 'rm -rf "$INSTALLER_TMP_ROOT" "$TMP_EXTRACT"' EXIT
mkdir -p "$INSTALLER_TMP_ROOT/node-agent"
cp "$TMP_EXTRACT/hermes-node-agent/hermes_node_agent.py" "$INSTALLER_TMP_ROOT/node-agent/hermes_node_agent.py"
cp "$TMP_EXTRACT/hermes-node-agent/browser_controller.py" "$INSTALLER_TMP_ROOT/node-agent/browser_controller.py"
cp "$TMP_EXTRACT/hermes-node-agent/requirements.txt" "$INSTALLER_TMP_ROOT/node-agent/requirements.txt"
cp "$TMP_EXTRACT/hermes-node-agent/install.sh" "$INSTALLER_TMP_ROOT/node-agent/install.sh"
cp "$TMP_EXTRACT/hermes-node-agent/hermes-node-agent.init.d" "$INSTALLER_TMP_ROOT/node-agent/hermes-node-agent.init.d"
cp "$TMP_EXTRACT/hermes-node-agent/hermes-node-agent.service" "$INSTALLER_TMP_ROOT/node-agent/hermes-node-agent.service"
cp "$TMP_EXTRACT/hermes-node-agent/README.md" "$INSTALLER_TMP_ROOT/node-agent/README.md"
cp "$TMP_EXTRACT/hermes-node-agent/LICENSE" "$INSTALLER_TMP_ROOT/node-agent/LICENSE"
cp "$TMP_EXTRACT/hermes-node-agent/DEPLOYMENT.md" "$INSTALLER_TMP_ROOT/node-agent/DEPLOYMENT.md"
cp "$TMP_EXTRACT/hermes-node-agent/PROTOCOL.md" "$INSTALLER_TMP_ROOT/node-agent/PROTOCOL.md"
cp "$TMP_EXTRACT/hermes-node-agent/BROWSER_PROTOCOL.md" "$INSTALLER_TMP_ROOT/node-agent/BROWSER_PROTOCOL.md"
(
cd "$INSTALLER_TMP_ROOT"
bash ./node-agent/install.sh
)
rm -rf "$INSTALLER_TMP_ROOT"
trap 'rm -rf "$TMP_EXTRACT"' EXIT
else
echo "❌ ERROR: install.sh missing from embedded payload"
exit 1
fi fi
echo echo "[3/5] Cleaning up temporary files..."
echo "Select capabilities [Y/n]:" cd "$ORIG_PWD"
read -p " Enable exec? (Y/n): " E_EXEC rm -rf "$TMP_EXTRACT"
[[ "$E_EXEC" =~ ^[Nn]$ ]] && EXEC_CAP="" || EXEC_CAP="exec" trap - EXIT
read -p " Enable browser_control? (y/N): " E_BROWSER
[[ "$E_BROWSER" =~ ^[Yy]$ ]] && BROWSER_CAP="browser_control" || BROWSER_CAP="" # Install SysV init service (root only)
read -p " Enable computer_control? (y/N): " E_COMPUTER if [ "$USE_SERVICE" = true ]; then
[[ "$E_COMPUTER" =~ ^[Yy]$ ]] && COMPUTER_CAP="computer_control" || COMPUTER_CAP="" echo ""
echo "[Service] Enabling auto-start on boot..."
CAPABILITIES="[" update-rc.d hermes-node-agent defaults 2>/dev/null || true
FIRST=1 echo "✓ Service: /etc/init.d/hermes-node-agent"
for cap in $EXEC_CAP $BROWSER_CAP $COMPUTER_CAP; do echo ""
[ -n "$cap" ] || continue echo "Service commands:"
[ $FIRST -eq 0 ] && CAPABILITIES="$CAPABILITIES, " echo " /etc/init.d/hermes-node-agent start|stop|restart|status"
CAPABILITIES="$CAPABILITIES"$cap"" else
FIRST=0 echo ""
done echo "[Service] Skipped (not root). Manual start required:"
CAPABILITIES="$CAPABILITIES]" echo " $AGENT_DIR/hermes-node-agent --config $CONFIG_DIR/config.json"
read -p "Enable sexec? (y/N): " E_SEXEC
SEXEC_LINE=""
if [[ "$E_SEXEC" =~ ^[Yy]$ ]]; then
SEXEC_LINE=" \"sexec_path\": \"$HOME/.config/hermes-node-agent/sexec/sexec.sh\","
echo "✓ sexec will be enabled"
fi fi
NODE_NAME=$(hostname) echo ""
echo "=== Installation Complete ==="
cat > "$CONFIG_DIR/config.json" <<END echo ""
{ echo "Quick start:"
"gateway_url": "wss://${GATEWAY_HOST}${GATEWP}",
"node_name": "${NODE_NAME}",
"token": "${NODE_TOKEN}",
$SEXEC_LINE
"reconnect_interval": 5,
"heartbeat_interval": 30,
"capabilities": $CAPABILITIES,
"protocol_version": "1.0"
}
END
echo "✓ Config: $CONFIG_DIR/config.json"
echo
echo "[4/5] Verifying installation..."
python3 -c "import hermes_node_agent" 2>/dev/null && echo "✓ Agent module OK" || true
echo
echo "[5/5] Done ==="
echo " Agent: $AGENT_DIR/hermes-node-agent"
echo " Config: $CONFIG_DIR/config.json" echo " Config: $CONFIG_DIR/config.json"
echo echo " Agent: $AGENT_DIR/hermes-node-agent"
if [ "$RUN_AS_ROOT" = true ]; then echo " Run: $AGENT_DIR/hermes-node-agent --config $CONFIG_DIR/config.json"
echo " Run: /etc/init.d/hermes-node-agent start" echo ""
if [ "$USE_SERVICE" = true ]; then
echo "To start the service:"
echo " /etc/init.d/hermes-node-agent start"
echo ""
else else
echo " Run: $AGENT_DIR/hermes-node-agent --config $CONFIG_DIR/config.json &" echo "To run manually:"
echo " $AGENT_DIR/hermes-node-agent --config $CONFIG_DIR/config.json &"
echo ""
fi fi
#!/bin/bash #!/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.
# Complete Windows installer build: PyInstaller + NSIS # Complete Windows installer build: PyInstaller + NSIS
# Works on Linux using Wine Windows Python environment # Works on Linux using Wine Windows Python environment
# #
......
#!/bin/bash #!/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.
# Build Windows offline installer for Hermes Node Agent # Build Windows offline installer for Hermes Node Agent
# Requires: makensis (NSIS compiler), wget/curl, python3 + pip # Requires: makensis (NSIS compiler), wget/curl, python3 + pip
......
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/usr/bin/env python3 #!/usr/bin/env python3
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
...@@ -15,17 +21,152 @@ Author: Lisa (Hermes AI) ...@@ -15,17 +21,152 @@ Author: Lisa (Hermes AI)
Date: 2026-04-30 (enhanced) Date: 2026-04-30 (enhanced)
""" """
import argparse
import asyncio import asyncio
import base64
import json import json
import logging import logging
import os import os
import shutil
import shlex
import ssl
import subprocess import subprocess
import sys import sys
import time import time
import argparse
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
LOG_PREVIEW_LEN = 120
def _preview_command(command: Any, limit: int = LOG_PREVIEW_LEN) -> str:
"""Return a compact single-line preview for logging."""
if isinstance(command, (list, tuple)):
text = ' '.join(str(part) for part in command)
else:
text = str(command)
text = ' '.join(text.split())
if len(text) > limit:
return text[:limit] + '…'
return text
def _setup_logging(debug: bool = False) -> None:
level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(level=level, format='%(message)s')
def _log_start(node_name: str, tools: list) -> None:
logger.info(f"start ▶ {node_name} — {', '.join(tools)}")
def _log_connect(url: str) -> None:
logger.info(f"connect ▶ {url}")
def _log_tls_disabled() -> None:
logger.info("tls verify disabled")
def _log_connected() -> None:
logger.info("connect ✓")
def _log_disconnected(reason: Any = None) -> None:
if reason:
logger.info(f"disconnect — {reason}")
else:
logger.info("disconnect")
def _log_registering(node_name: str) -> None:
logger.info(f"register ▶ {node_name}")
def _log_registered(node_name: str) -> None:
logger.info(f"register ✓ {node_name}")
def _log_waiting() -> None:
logger.info("waiting for commands")
def _log_exec_received(command: Any) -> None:
logger.info(f"exec ▶ {_preview_command(command)}")
def _log_exec_completed(command: Any, exit_code: Any) -> None:
logger.info(f"exec ✓ exit={exit_code} — {_preview_command(command)}")
def _log_exec_failed(command: Any, error: Any, exit_code: Any = None) -> None:
prefix = f"exec ✗ exit={exit_code}" if exit_code is not None else "exec ✗"
logger.error(f"{prefix} — {_preview_command(command)} — {error}")
def _log_tool_completed(tool_name: str, label: Any, success: bool, error: Any = None) -> None:
mark = '✓' if success else '✗'
suffix = f" — {error}" if error else ''
logger.info(f"{tool_name} {mark} {_preview_command(label)}{suffix}")
def _log_browser_received(command: Any) -> None:
logger.info(f"browser ▶ {_preview_command(command)}")
def _log_cc_received(action: Any) -> None:
logger.info(f"computer ▶ {_preview_command(action)}")
def _log_audio_received(action: Any) -> None:
logger.info(f"audio ▶ {_preview_command(action)}")
def _log_camera_received(action: Any) -> None:
logger.info(f"camera ▶ {_preview_command(action)}")
def _log_reconnect(delay: Any, reason: Any) -> None:
logger.info(f"reconnect in {delay}s — {reason}")
def _log_registration_ack() -> None:
logger.debug("register ack")
def _log_heartbeat_ack() -> None:
logger.debug("heartbeat ack")
def _log_unknown_message(req_type: Any) -> None:
logger.warning(f"unknown message: {req_type}")
def _log_connection_error(message: Any) -> None:
logger.error(f"connection error — {message}")
def _log_config_missing(path: Path) -> None:
logger.error(f"config missing — {path}")
def _log_token_missing() -> None:
logger.error("token missing in config")
def _log_init_warning(component: str, message: Any) -> None:
logger.warning(f"{component} init failed — {message}")
def _log_disabled(component: str, message: str) -> None:
logger.warning(f"{component} disabled — {message}")
def _log_shutdown() -> None:
logger.info("shutdown")
logger = logging.getLogger(__name__)
try: try:
import websockets import websockets
except ImportError: except ImportError:
...@@ -50,7 +191,6 @@ DEFAULT_CONFIG = { ...@@ -50,7 +191,6 @@ DEFAULT_CONFIG = {
'gateway_url': 'wss://localhost:8765', 'gateway_url': 'wss://localhost:8765',
'node_name': 'unknown', 'node_name': 'unknown',
'token': DEFAULT_GATEWAY_TOKEN, 'token': DEFAULT_GATEWAY_TOKEN,
'sexec_path': '~/.config/hermes-node/sexec/sexec.sh',
'reconnect_interval': 5, 'reconnect_interval': 5,
'heartbeat_interval': 30, 'heartbeat_interval': 30,
'gateway_cert_path': None, 'gateway_cert_path': None,
...@@ -59,6 +199,7 @@ DEFAULT_CONFIG = { ...@@ -59,6 +199,7 @@ DEFAULT_CONFIG = {
'enable_computer_control': False, 'enable_computer_control': False,
'enable_desktop_observe': False, 'enable_desktop_observe': False,
'enable_audio_control': False, 'enable_audio_control': False,
'enable_camera_control': False,
} }
# ═════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════
...@@ -87,16 +228,22 @@ class CommandExecutor: ...@@ -87,16 +228,22 @@ class CommandExecutor:
def __init__(self, permission_rules: Dict[str, List[str]]): def __init__(self, permission_rules: Dict[str, List[str]]):
self.permissions = permission_rules or {'allow': [], 'deny': [], 'ask': []} self.permissions = permission_rules or {'allow': [], 'deny': [], 'ask': []}
def execute(self, command: str, approved: bool = False) -> Dict[str, Any]: def execute(self, command: Any, approved: bool = False) -> Dict[str, Any]:
"""Execute command respecting permission rules.""" """Execute command respecting permission rules."""
raise NotImplementedError raise NotImplementedError
def _check_permission(self, command: str, approved: bool) -> tuple[bool, str]: def _normalize_command_text(self, command: Any) -> str:
if isinstance(command, (list, tuple)):
return ' '.join(str(part) for part in command).strip()
return str(command).strip()
def _check_permission(self, command: Any, approved: bool) -> tuple[bool, str]:
"""Check allow/deny/ask rules. """Check allow/deny/ask rules.
Returns (allowed, reason). 'ask' means requires approval gate. Returns (allowed, reason). 'ask' means requires approval gate.
Accepts command as string or argv list.
""" """
import re import re
cmd = command.strip() cmd = self._normalize_command_text(command)
# Deny (highest priority) # Deny (highest priority)
for pattern in self.permissions.get('deny', []): for pattern in self.permissions.get('deny', []):
if re.search(pattern, cmd, re.IGNORECASE): if re.search(pattern, cmd, re.IGNORECASE):
...@@ -117,30 +264,38 @@ class CommandExecutor: ...@@ -117,30 +264,38 @@ class CommandExecutor:
# ── POSIX ────────────────────────────────────────────────────────────────── # ── POSIX ──────────────────────────────────────────────────────────────────
class PosixCommandExecutor(CommandExecutor): class PosixCommandExecutor(CommandExecutor):
"""POSIX implementation — uses sexec.sh and environment variable.""" """POSIX implementation using integrated permission checks and /bin/sh."""
def __init__(self, sexec_path: str, permission_rules: Dict[str, List[str]]):
super().__init__(permission_rules)
self.sexec_path = Path(sexec_path).expanduser()
def execute(self, command: str, approved: bool = False) -> Dict[str, Any]: def execute(self, command: Any, approved: bool = False) -> Dict[str, Any]:
allowed, reason = self._check_permission(command, approved) allowed, reason = self._check_permission(command, approved)
if not allowed and reason != 'ask': if not allowed and reason != 'ask':
return {'success': False, 'error': f'Permission denied: {reason}', 'exit_code': 127} return {'success': False, 'error': f'Permission denied: {reason}', 'exit_code': 127}
if not approved and reason == 'ask': if not approved and reason == 'ask':
return {'success': False, 'error': 'Command requires approval', 'exit_code': 2} return {'success': False, 'error': 'Command requires approval', 'exit_code': 2}
if not self.sexec_path.exists(): if isinstance(command, (list, tuple)):
return {'success': False, 'error': f'sexec not found: {self.sexec_path}', 'exit_code': 127} cmd = ' '.join(shlex.quote(str(part)) for part in command)
else:
cmd = str(command)
try: try:
env = os.environ.copy()
env['SEXEC_COMMAND'] = command
proc = subprocess.Popen( proc = subprocess.Popen(
[str(self.sexec_path)], ['/bin/sh', '-c', cmd],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, text=True stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
) )
out, err = proc.communicate(timeout=300) out, err = proc.communicate(timeout=300)
return {'success': proc.returncode == 0, 'stdout': out, 'stderr': err, return {
'exit_code': proc.returncode} 'success': proc.returncode == 0,
'stdout': out,
'stderr': err,
'exit_code': proc.returncode,
}
except subprocess.TimeoutExpired:
try:
proc.kill()
except Exception:
pass
return {'success': False, 'error': 'Command timed out', 'exit_code': 124}
except Exception as e: except Exception as e:
return {'success': False, 'error': str(e), 'exit_code': -1} return {'success': False, 'error': str(e), 'exit_code': -1}
...@@ -385,15 +540,291 @@ class WindowsComputerController(ComputerControllerBase): ...@@ -385,15 +540,291 @@ class WindowsComputerController(ComputerControllerBase):
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
# ── AUDIO CONTROL ──────────────────────────────────────────────────────────
class AudioControllerBase:
"""Base class for audio device/media actions."""
def capability_info(self) -> Dict[str, Any]:
raise NotImplementedError
def list_audio_devices(self) -> Dict[str, Any]:
raise NotImplementedError
def get_audio_status(self) -> Dict[str, Any]:
raise NotImplementedError
def capture_output(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
def capture_input(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
def play_audio(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
class CameraControllerBase:
"""Base class for camera device/media actions."""
def capability_info(self) -> Dict[str, Any]:
raise NotImplementedError
def list_cameras(self) -> Dict[str, Any]:
raise NotImplementedError
def get_camera_status(self) -> Dict[str, Any]:
raise NotImplementedError
def capture_frame(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
def capture_video(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
class PosixAudioController(AudioControllerBase):
"""Linux audio support via ffmpeg + available host backends."""
def __init__(self):
self.ffmpeg = shutil.which('ffmpeg')
if not self.ffmpeg:
raise PlatformError('ffmpeg not found')
self.ffplay = shutil.which('ffplay')
self.ffprobe = shutil.which('ffprobe')
self.pactl = shutil.which('pactl')
self.arecord = shutil.which('arecord')
self.aplay = shutil.which('aplay')
self.backend = self._detect_backend()
def _detect_backend(self) -> str:
if self.pactl:
probe = self._run_quiet([self.pactl, 'info'])
if probe['success']:
server = (probe.get('stdout') or '').lower()
if 'pipewire' in server:
return 'pipewire-pulse'
return 'pulseaudio'
if os.environ.get('PIPEWIRE_RUNTIME_DIR') or os.environ.get('XDG_RUNTIME_DIR'):
return 'pipewire'
if self.arecord:
return 'alsa'
return 'unknown'
def capability_info(self) -> Dict[str, Any]:
monitor_ready, monitor_name = self._default_monitor_source()
input_ready, input_source = self._default_input_source()
return {
'platform': 'linux',
'backend': self.backend,
'available': True,
'can_capture_output': monitor_ready,
'can_capture_input': input_ready,
'can_play_audio': bool(self.ffplay or self.aplay or self.ffmpeg),
'can_inject_mic': False,
'capture_output_ready': monitor_ready,
'capture_output_backend': 'pulseaudio-monitor' if monitor_ready else None,
'default_output_monitor': monitor_name,
'default_input_source': input_source,
'ffmpeg': bool(self.ffmpeg),
'ffplay': bool(self.ffplay),
'pactl': bool(self.pactl),
'arecord': bool(self.arecord),
'aplay': bool(self.aplay),
}
def _run_quiet(self, cmd: List[str], timeout: int = 15) -> Dict[str, Any]:
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return {
'success': proc.returncode == 0,
'stdout': proc.stdout,
'stderr': proc.stderr,
'exit_code': proc.returncode,
}
except Exception as e:
return {'success': False, 'stdout': '', 'stderr': str(e), 'exit_code': -1}
def _default_output_sink(self) -> Optional[str]:
if not self.pactl:
return None
result = self._run_quiet([self.pactl, 'get-default-sink'])
sink = (result.get('stdout') or '').strip()
if result['success'] and sink:
return sink
return None
def _default_input_source(self) -> tuple[bool, Optional[str]]:
if self.pactl:
result = self._run_quiet([self.pactl, 'get-default-source'])
source = (result.get('stdout') or '').strip()
if result['success'] and source:
return True, source
if self.arecord:
return True, 'default'
return False, None
def _default_monitor_source(self) -> tuple[bool, Optional[str]]:
sink = self._default_output_sink()
if sink:
return True, f'{sink}.monitor'
return False, None
def _expand_output_path(self, path: Optional[str], suffix: str) -> str:
if path:
return str(Path(path).expanduser())
stamp = int(time.time())
return f'/tmp/hermes-audio-{stamp}{suffix}'
def _encode_file(self, path: str) -> Dict[str, Any]:
data = Path(path).read_bytes()
return {
'path': path,
'size_bytes': len(data),
'data_base64': base64.b64encode(data).decode('ascii'),
}
def _media_duration(self, path: str) -> Optional[float]:
if not self.ffprobe:
return None
result = self._run_quiet([
self.ffprobe, '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', path
])
if not result['success']:
return None
try:
return round(float((result.get('stdout') or '').strip()), 3)
except Exception:
return None
def list_audio_devices(self) -> Dict[str, Any]:
devices: Dict[str, Any] = {'backend': self.backend, 'sinks': [], 'sources': []}
if self.pactl:
sink_res = self._run_quiet([self.pactl, 'list', 'short', 'sinks'])
source_res = self._run_quiet([self.pactl, 'list', 'short', 'sources'])
if sink_res['success']:
for line in sink_res.get('stdout', '').splitlines():
parts = line.split('\t')
if len(parts) >= 2:
devices['sinks'].append({'id': parts[0], 'name': parts[1], 'raw': line})
if source_res['success']:
for line in source_res.get('stdout', '').splitlines():
parts = line.split('\t')
if len(parts) >= 2:
devices['sources'].append({'id': parts[0], 'name': parts[1], 'raw': line})
if not devices['sources'] and self.arecord:
src = self._run_quiet([self.arecord, '-l'])
devices['sources_raw'] = src.get('stdout', '')
return {'success': True, **devices}
def get_audio_status(self) -> Dict[str, Any]:
monitor_ready, monitor_name = self._default_monitor_source()
input_ready, input_source = self._default_input_source()
status = {
'success': True,
'backend': self.backend,
'default_output_sink': self._default_output_sink(),
'default_output_monitor': monitor_name,
'default_input_source': input_source,
'capture_output_ready': monitor_ready,
'capture_input_ready': input_ready,
'can_play_audio': bool(self.ffplay or self.aplay or self.ffmpeg),
}
if self.pactl:
info = self._run_quiet([self.pactl, 'info'])
if info['success']:
status['server_info'] = info.get('stdout', '')
return status
def capture_output(self, params: Dict[str, Any]) -> Dict[str, Any]:
duration = max(1, min(int(params.get('duration', 5)), 600))
fmt = str(params.get('format', 'wav')).lower()
path = self._expand_output_path(params.get('output_path') or params.get('path'), f'.{fmt}')
monitor_ready, monitor = self._default_monitor_source()
if not monitor_ready or not monitor:
return {'success': False, 'error': 'No PulseAudio/PipeWire monitor source available for output capture'}
cmd = [
self.ffmpeg, '-y', '-v', 'error', '-f', 'pulse', '-i', monitor,
'-t', str(duration), path,
]
result = self._run_quiet(cmd, timeout=duration + 15)
if not result['success']:
return {'success': False, 'error': (result.get('stderr') or result.get('stdout') or 'ffmpeg capture failed').strip()}
payload = {
'success': True,
'format': fmt,
'duration': duration,
'source': monitor,
}
payload.update(self._encode_file(path))
media_duration = self._media_duration(path)
if media_duration is not None:
payload['measured_duration'] = media_duration
return payload
def capture_input(self, params: Dict[str, Any]) -> Dict[str, Any]:
duration = max(1, min(int(params.get('duration', 5)), 600))
fmt = str(params.get('format', 'wav')).lower()
path = self._expand_output_path(params.get('output_path') or params.get('path'), f'.{fmt}')
source = params.get('source')
input_ready, default_source = self._default_input_source()
if not source:
source = default_source
if self.pactl and source:
cmd = [
self.ffmpeg, '-y', '-v', 'error', '-f', 'pulse', '-i', str(source),
'-t', str(duration), path,
]
result = self._run_quiet(cmd, timeout=duration + 15)
elif self.arecord and input_ready:
cmd = [self.arecord, '-q', '-d', str(duration), path]
result = self._run_quiet(cmd, timeout=duration + 15)
else:
return {'success': False, 'error': 'No usable input capture backend available'}
if not result['success']:
return {'success': False, 'error': (result.get('stderr') or result.get('stdout') or 'input capture failed').strip()}
payload = {
'success': True,
'format': fmt,
'duration': duration,
'source': source,
}
payload.update(self._encode_file(path))
media_duration = self._media_duration(path)
if media_duration is not None:
payload['measured_duration'] = media_duration
return payload
def play_audio(self, params: Dict[str, Any]) -> Dict[str, Any]:
path = params.get('path')
if not path:
return {'success': False, 'error': 'play_audio requires params.path'}
path = str(Path(path).expanduser())
if not Path(path).exists():
return {'success': False, 'error': f'Audio file not found: {path}'}
if self.ffplay:
cmd = [self.ffplay, '-nodisp', '-autoexit', '-loglevel', 'error', path]
elif self.aplay:
cmd = [self.aplay, path]
else:
cmd = [self.ffmpeg, '-v', 'error', '-i', path, '-f', 'null', '-']
result = self._run_quiet(cmd, timeout=max(30, int(params.get('timeout', 120))))
if not result['success']:
return {'success': False, 'error': (result.get('stderr') or result.get('stdout') or 'audio playback failed').strip()}
payload = {'success': True, 'path': path}
media_duration = self._media_duration(path)
if media_duration is not None:
payload['duration'] = media_duration
return payload
# ── Factory functions ───────────────────────────────────────────────────── # ── Factory functions ─────────────────────────────────────────────────────
def make_executor(config: Dict[str, Any]) -> CommandExecutor: def make_executor(config: Dict[str, Any]) -> CommandExecutor:
"""Select appropriate command executor for current platform.""" """Select appropriate command executor for current platform."""
perms = config.get('permissions', {}) perms = config.get('permissions', {})
if is_windows(): if is_windows():
return WindowsCommandExecutor(perms) return WindowsCommandExecutor(perms)
else: return PosixCommandExecutor(perms)
sexec = config.get('sexec_path', str(Path.home() / '.openclaw/skills/sexec/sexec.sh'))
return PosixCommandExecutor(sexec, perms)
def make_computer_controller(config: Dict[str, Any]) -> Optional[ComputerControllerBase]: def make_computer_controller(config: Dict[str, Any]) -> Optional[ComputerControllerBase]:
"""Select and instantiate the appropriate computer controller, or None if deps missing.""" """Select and instantiate the appropriate computer controller, or None if deps missing."""
...@@ -421,11 +852,254 @@ def make_computer_controller(config: Dict[str, Any]) -> Optional[ComputerControl ...@@ -421,11 +852,254 @@ def make_computer_controller(config: Dict[str, Any]) -> Optional[ComputerControl
return None return None
def make_audio_controller(config: Dict[str, Any]) -> Optional[AudioControllerBase]:
if not config.get('enable_audio_control'):
return None
if is_linux():
try:
return PosixAudioController()
except Exception as e:
_log_disabled('audio_control', str(e))
return None
_log_disabled('audio_control', f'unsupported platform: {sys.platform}')
return None
class PosixCameraController(CameraControllerBase):
"""Linux camera support via ffmpeg + V4L2 device nodes."""
def __init__(self):
self.ffmpeg = shutil.which('ffmpeg')
if not self.ffmpeg:
raise PlatformError('ffmpeg not found')
self.ffprobe = shutil.which('ffprobe')
self.v4l2_ctl = shutil.which('v4l2-ctl')
def _list_device_paths(self) -> List[Path]:
return sorted(Path('/dev').glob('video*'), key=lambda p: p.name)
def _encode_file(self, path: str) -> Dict[str, Any]:
data = Path(path).read_bytes()
return {
'path': path,
'size_bytes': len(data),
'data_base64': base64.b64encode(data).decode('ascii'),
}
def _media_duration(self, path: str) -> Optional[float]:
if not self.ffprobe:
return None
try:
proc = subprocess.run([
self.ffprobe, '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', path
], capture_output=True, text=True, timeout=15)
if proc.returncode != 0:
return None
return round(float((proc.stdout or '').strip()), 3)
except Exception:
return None
def _expand_output_path(self, path: Optional[str], suffix: str) -> str:
if path:
return str(Path(path).expanduser())
stamp = int(time.time())
return f'/tmp/hermes-camera-{stamp}{suffix}'
def _ffmpeg_probe(self, device: str) -> Dict[str, Any]:
try:
proc = subprocess.run(
[self.ffmpeg, '-hide_banner', '-f', 'v4l2', '-list_formats', 'all', '-i', device],
capture_output=True,
text=True,
timeout=15,
)
text = '\n'.join(part for part in [proc.stdout, proc.stderr] if part)
return {
'success': proc.returncode in (0, 1),
'output': text.strip(),
'exit_code': proc.returncode,
}
except Exception as e:
return {'success': False, 'output': str(e), 'exit_code': -1}
def _device_info(self, device_path: Path) -> Dict[str, Any]:
info = {
'path': str(device_path),
'name': device_path.name,
'exists': device_path.exists(),
'readable': os.access(device_path, os.R_OK),
'writable': os.access(device_path, os.W_OK),
}
by_id_root = Path('/dev/v4l/by-id')
aliases = []
if by_id_root.exists():
for alias in sorted(by_id_root.iterdir()):
try:
if alias.resolve() == device_path.resolve():
aliases.append(str(alias))
except Exception:
continue
if aliases:
info['aliases'] = aliases
if self.v4l2_ctl:
try:
proc = subprocess.run(
[self.v4l2_ctl, '--device', str(device_path), '--all'],
capture_output=True,
text=True,
timeout=15,
)
info['details'] = (proc.stdout or proc.stderr or '').strip()
info['available'] = proc.returncode == 0
except Exception as e:
info['available'] = False
info['probe_error'] = str(e)
else:
probe = self._ffmpeg_probe(str(device_path))
info['available'] = probe['success']
if probe.get('output'):
info['details'] = probe['output']
return info
def capability_info(self) -> Dict[str, Any]:
devices = self._list_device_paths()
return {
'platform': 'linux',
'backend': 'v4l2-ffmpeg',
'available': bool(devices),
'device_count': len(devices),
'supports_frame_capture': True,
'supports_video_capture': True,
'ffmpeg': bool(self.ffmpeg),
'ffprobe': bool(self.ffprobe),
'v4l2_ctl': bool(self.v4l2_ctl),
'devices': [str(p) for p in devices],
}
def list_cameras(self) -> Dict[str, Any]:
devices = [self._device_info(path) for path in self._list_device_paths()]
return {
'success': True,
'backend': 'v4l2-ffmpeg',
'camera_count': len(devices),
'cameras': devices,
}
def get_camera_status(self) -> Dict[str, Any]:
devices = self.list_cameras()
payload = {
'success': True,
'backend': 'v4l2-ffmpeg',
'ffmpeg': bool(self.ffmpeg),
'ffprobe': bool(self.ffprobe),
'v4l2_ctl': bool(self.v4l2_ctl),
'camera_count': devices.get('camera_count', 0),
'cameras': devices.get('cameras', []),
}
if payload['camera_count'] == 0:
payload['available'] = False
payload['reason'] = 'No /dev/video* devices found'
else:
payload['available'] = True
return payload
def _pick_device(self, params: Dict[str, Any]) -> str:
device = params.get('device') or params.get('device_path')
if device:
return str(Path(str(device)).expanduser())
devices = self._list_device_paths()
if not devices:
raise PlatformError('No /dev/video* devices found')
return str(devices[0])
def capture_frame(self, params: Dict[str, Any]) -> Dict[str, Any]:
device = self._pick_device(params)
fmt = str(params.get('format', 'png')).lower()
if fmt not in ('png', 'jpg', 'jpeg', 'bmp'):
return {'success': False, 'error': f'Unsupported frame format: {fmt}'}
suffix = '.jpg' if fmt == 'jpeg' else f'.{fmt}'
path = self._expand_output_path(params.get('output_path') or params.get('path'), suffix)
width = params.get('width')
height = params.get('height')
cmd = [self.ffmpeg, '-y', '-v', 'error', '-f', 'v4l2']
if width and height:
cmd += ['-video_size', f'{int(width)}x{int(height)}']
cmd += ['-i', device, '-frames:v', '1', path]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
except Exception as e:
return {'success': False, 'error': str(e)}
if proc.returncode != 0:
return {'success': False, 'error': (proc.stderr or proc.stdout or 'frame capture failed').strip()}
payload = {
'success': True,
'device': device,
'format': fmt,
}
payload.update(self._encode_file(path))
return payload
def capture_video(self, params: Dict[str, Any]) -> Dict[str, Any]:
device = self._pick_device(params)
duration = max(1, min(int(params.get('duration', 5)), 600))
fmt = str(params.get('format', 'mp4')).lower()
extension = 'mkv' if fmt == 'matroska' else fmt
if extension not in ('mp4', 'mkv', 'webm'):
return {'success': False, 'error': f'Unsupported video format: {fmt}'}
path = self._expand_output_path(params.get('output_path') or params.get('path'), f'.{extension}')
width = params.get('width')
height = params.get('height')
fps = params.get('fps')
cmd = [self.ffmpeg, '-y', '-v', 'error', '-f', 'v4l2']
if fps:
cmd += ['-framerate', str(fps)]
if width and height:
cmd += ['-video_size', f'{int(width)}x{int(height)}']
cmd += ['-i', device, '-t', str(duration), path]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=duration + 30)
except Exception as e:
return {'success': False, 'error': str(e)}
if proc.returncode != 0:
return {'success': False, 'error': (proc.stderr or proc.stdout or 'video capture failed').strip()}
payload = {
'success': True,
'device': device,
'format': extension,
'duration': duration,
}
payload.update(self._encode_file(path))
media_duration = self._media_duration(path)
if media_duration is not None:
payload['measured_duration'] = media_duration
return payload
def make_camera_controller(config: Dict[str, Any]) -> Optional[CameraControllerBase]:
if not config.get('enable_camera_control'):
return None
if is_linux():
try:
controller = PosixCameraController()
if not controller._list_device_paths():
_log_disabled('camera_control', 'no /dev/video* devices found')
return None
return controller
except Exception as e:
_log_disabled('camera_control', str(e))
return None
_log_disabled('camera_control', f'unsupported platform: {sys.platform}')
return None
class NodeAgent: class NodeAgent:
def __init__(self, config_path: Optional[str] = None): def __init__(self, config_path: Optional[str] = None):
self.config = self._load_config(config_path) self.config = self._load_config(config_path)
self.executor = make_command_executor(self.config) self.executor = make_executor(self.config)
self.computer = make_computer_controller(self.config) self.computer = make_computer_controller(self.config)
self.audio = make_audio_controller(self.config)
self.camera = make_camera_controller(self.config)
self.browser = None self.browser = None
if self.config.get('enable_browser') and HAS_BROWSER: if self.config.get('enable_browser') and HAS_BROWSER:
try: try:
...@@ -462,6 +1136,7 @@ class NodeAgent: ...@@ -462,6 +1136,7 @@ class NodeAgent:
'enable_computer_control': self.config.get('enable_computer_control', False), 'enable_computer_control': self.config.get('enable_computer_control', False),
'enable_desktop_observe': self.config.get('enable_desktop_observe', False), 'enable_desktop_observe': self.config.get('enable_desktop_observe', False),
'enable_audio_control': self.config.get('enable_audio_control', False), 'enable_audio_control': self.config.get('enable_audio_control', False),
'enable_camera_control': self.config.get('enable_camera_control', False),
} }
if self.browser is not None: if self.browser is not None:
caps['browser_control'] = {'available': True} caps['browser_control'] = {'available': True}
...@@ -479,45 +1154,74 @@ class NodeAgent: ...@@ -479,45 +1154,74 @@ class NodeAgent:
'display': os.environ.get('DISPLAY', ':0') 'display': os.environ.get('DISPLAY', ':0')
} }
if caps['enable_audio_control']: if caps['enable_audio_control']:
if self.audio is not None:
caps['audio_control'] = self.audio.capability_info()
else:
caps['audio_control'] = { caps['audio_control'] = {
'available': False, 'available': False,
'reason': 'audio control implementation not present in this agent build' 'reason': 'audio control requested but backend dependencies are unavailable'
}
if caps['enable_camera_control']:
if self.camera is not None:
caps['camera_control'] = self.camera.capability_info()
else:
caps['camera_control'] = {
'available': False,
'reason': 'camera control requested but no supported camera backend/devices are available'
} }
return caps return caps
async def connect_and_run(self): async def connect_and_run(self):
"""Main loop: connect to gateway and process commands.""" """Main loop: connect to gateway and process commands."""
url = f"{self.config['gateway_url']}?node_name={self.config['node_name']}&token={self.config['token']}" url = f"{self.config['gateway_url']}?node_name={self.config['node_name']}&token={self.config['token']}"
logger.info(f"Connecting to {url}") _log_connect(url)
ssl_context = None
if url.startswith('wss://'):
cert_path = self.config.get('gateway_cert_path')
if cert_path:
ssl_context = ssl.create_default_context(cafile=str(Path(cert_path).expanduser()))
else:
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
_log_tls_disabled()
while True: while True:
try: try:
async with websockets.connect(url, ping_interval=20, ping_timeout=10) as ws: async with websockets.connect(url, ping_interval=20, ping_timeout=10, ssl=ssl_context) as ws:
logger.info("Connected to gateway — awaiting commands") _log_connected()
# Send capabilities announcement # Send registration frame expected by the gateway
_log_registering(self.config['node_name'])
await ws.send(json.dumps({ await ws.send(json.dumps({
"type": "capabilities", "type": "register",
"node_name": self.config['node_name'], "node_name": self.config['node_name'],
"version": "1.0",
"tools": self._get_available_tools(), "tools": self._get_available_tools(),
"capabilities": self.capabilities "capabilities": self.capabilities
})) }))
_log_waiting()
heartbeat_task = asyncio.create_task(self._heartbeat_loop(ws)) heartbeat_task = asyncio.create_task(self._heartbeat_loop(ws))
disconnect_reason = None
try: try:
async for raw in ws: async for raw in ws:
msg = json.loads(raw) msg = json.loads(raw)
await self._handle_message(ws, msg) await self._handle_message(ws, msg)
except Exception as e:
disconnect_reason = e
raise
finally: finally:
heartbeat_task.cancel() heartbeat_task.cancel()
try: try:
await heartbeat_task await heartbeat_task
except asyncio.CancelledError: except asyncio.CancelledError:
pass pass
_log_disconnected(disconnect_reason)
except Exception as e: except Exception as e:
logger.error(f"Connection error: {e}") _log_connection_error(e)
logger.info(f"Reconnecting in {self.config['reconnect_interval']}s...") _log_reconnect(self.config['reconnect_interval'], e)
await asyncio.sleep(self.config['reconnect_interval']) await asyncio.sleep(self.config['reconnect_interval'])
def _get_available_tools(self) -> list: def _get_available_tools(self) -> list:
...@@ -529,8 +1233,10 @@ class NodeAgent: ...@@ -529,8 +1233,10 @@ class NodeAgent:
tools.append('computer_control') tools.append('computer_control')
if self.config.get('enable_desktop_observe') and self.computer is not None: if self.config.get('enable_desktop_observe') and self.computer is not None:
tools.append('desktop_observe') tools.append('desktop_observe')
if self.config.get('enable_audio_control'): if self.config.get('enable_audio_control') and self.audio is not None:
logger.warning('audio_control requested but not implemented in this node agent build') tools.append('audio_control')
if self.config.get('enable_camera_control') and self.camera is not None:
tools.append('camera_control')
return tools return tools
async def _handle_message(self, ws, msg: Dict[str, Any]): async def _handle_message(self, ws, msg: Dict[str, Any]):
...@@ -551,12 +1257,22 @@ class NodeAgent: ...@@ -551,12 +1257,22 @@ class NodeAgent:
command = msg.get('command') command = msg.get('command')
params = msg.get('params', {}) params = msg.get('params', {})
await self._handle_browser_control(ws, msg.get('id'), command, params) await self._handle_browser_control(ws, msg.get('id'), command, params)
elif req_type == 'audio_control':
action = msg['action']
params = msg.get('params', {})
await self._handle_audio_control(ws, msg.get('id'), action, params)
elif req_type == 'camera_control':
action = msg['action']
params = msg.get('params', {})
await self._handle_camera_control(ws, msg.get('id'), action, params)
elif req_type == 'register_ack': elif req_type == 'register_ack':
logger.info("Registration acknowledged by gateway") _log_registration_ack()
_log_registered(self.config['node_name'])
elif req_type == 'heartbeat_ack': elif req_type == 'heartbeat_ack':
_log_heartbeat_ack()
return return
else: else:
logger.warning(f"Unknown message type: {req_type}") _log_unknown_message(req_type)
async def _heartbeat_loop(self, ws): async def _heartbeat_loop(self, ws):
"""Send periodic heartbeats so the gateway can track liveness.""" """Send periodic heartbeats so the gateway can track liveness."""
...@@ -572,7 +1288,7 @@ class NodeAgent: ...@@ -572,7 +1288,7 @@ class NodeAgent:
except asyncio.CancelledError: except asyncio.CancelledError:
raise raise
except Exception as e: except Exception as e:
logger.warning(f"Heartbeat loop stopped: {e}") _log_connection_error(f"heartbeat loop stopped: {e}")
async def _send_json(self, ws, payload: Dict[str, Any]): async def _send_json(self, ws, payload: Dict[str, Any]):
await ws.send(json.dumps(payload)) await ws.send(json.dumps(payload))
...@@ -584,7 +1300,7 @@ class NodeAgent: ...@@ -584,7 +1300,7 @@ class NodeAgent:
- optional streamed chunks via ``exec_output`` - optional streamed chunks via ``exec_output``
- terminal result via ``exec_complete`` - terminal result via ``exec_complete``
""" """
logger.info(f"Exec: {command}") _log_exec_received(command)
try: try:
result = self.executor.execute(command, approved=approved) result = self.executor.execute(command, approved=approved)
stdout = result.get('stdout', '') or '' stdout = result.get('stdout', '') or ''
...@@ -603,13 +1319,20 @@ class NodeAgent: ...@@ -603,13 +1319,20 @@ class NodeAgent:
'stream': 'stderr', 'stream': 'stderr',
'data': stderr, 'data': stderr,
}) })
exit_code = result.get('exit_code', -1)
error = result.get('error')
if error:
_log_exec_failed(command, error, exit_code)
else:
_log_exec_completed(command, exit_code)
await self._send_json(ws, { await self._send_json(ws, {
'type': 'exec_complete', 'type': 'exec_complete',
'id': cmd_id, 'id': cmd_id,
'exit_code': result.get('exit_code', -1), 'exit_code': exit_code,
'error': result.get('error'), 'error': error,
}) })
except Exception as e: except Exception as e:
_log_exec_failed(command, str(e), -1)
await self._send_json(ws, { await self._send_json(ws, {
'type': 'exec_complete', 'type': 'exec_complete',
'id': cmd_id, 'id': cmd_id,
...@@ -618,15 +1341,13 @@ class NodeAgent: ...@@ -618,15 +1341,13 @@ class NodeAgent:
}) })
async def _handle_cc(self, ws, cmd_id: str, action: str, params: Dict[str, Any]): async def _handle_cc(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
_log_cc_received(action)
if self.computer is None: if self.computer is None:
await self._send_json(ws, { result = {
'type': 'computer_control_result',
'id': cmd_id,
'action': action,
'success': False, 'success': False,
'error': 'computer_control not available on this node', 'error': 'computer_control not available on this node',
}) }
return else:
try: try:
if action == 'screenshot': if action == 'screenshot':
result = self.computer.screenshot(params.get('output_path')) result = self.computer.screenshot(params.get('output_path'))
...@@ -646,20 +1367,19 @@ class NodeAgent: ...@@ -646,20 +1367,19 @@ class NodeAgent:
result = {'success': False, 'error': f'Unknown computer_control action: {action}'} result = {'success': False, 'error': f'Unknown computer_control action: {action}'}
except Exception as e: except Exception as e:
result = {'success': False, 'error': str(e)} result = {'success': False, 'error': str(e)}
_log_tool_completed('computer', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'computer_control_result', 'id': cmd_id, 'action': action} payload = {'type': 'computer_control_result', 'id': cmd_id, 'action': action}
payload.update(result) payload.update(result)
await self._send_json(ws, payload) await self._send_json(ws, payload)
async def _handle_desktop_observe(self, ws, cmd_id: str, action: str, params: Dict[str, Any]): async def _handle_desktop_observe(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
logger.info(f"observe ▶ {_preview_command(action)}")
if self.computer is None: if self.computer is None:
await self._send_json(ws, { result = {
'type': 'desktop_observe_result',
'id': cmd_id,
'action': action,
'success': False, 'success': False,
'error': 'desktop_observe requires computer_control support on this node', 'error': 'desktop_observe requires computer_control support on this node',
}) }
return else:
try: try:
if action in ('active_window', 'get_active_window'): if action in ('active_window', 'get_active_window'):
result = self.computer.get_active_window() result = self.computer.get_active_window()
...@@ -671,6 +1391,7 @@ class NodeAgent: ...@@ -671,6 +1391,7 @@ class NodeAgent:
result = {'success': False, 'error': f'Unknown desktop_observe action: {action}'} result = {'success': False, 'error': f'Unknown desktop_observe action: {action}'}
except Exception as e: except Exception as e:
result = {'success': False, 'error': str(e)} result = {'success': False, 'error': str(e)}
_log_tool_completed('observe', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'desktop_observe_result', 'id': cmd_id, 'action': action} payload = {'type': 'desktop_observe_result', 'id': cmd_id, 'action': action}
payload.update(result) payload.update(result)
await self._send_json(ws, payload) await self._send_json(ws, payload)
...@@ -685,6 +1406,7 @@ class NodeAgent: ...@@ -685,6 +1406,7 @@ class NodeAgent:
'error': 'browser_control not available on this node', 'error': 'browser_control not available on this node',
}) })
return return
_log_browser_received(command)
try: try:
if hasattr(self.browser, 'execute'): if hasattr(self.browser, 'execute'):
result = self.browser.execute(command, params) result = self.browser.execute(command, params)
...@@ -694,12 +1416,69 @@ class NodeAgent: ...@@ -694,12 +1416,69 @@ class NodeAgent:
result = {'success': False, 'error': 'BrowserController has no execute/run entrypoint'} result = {'success': False, 'error': 'BrowserController has no execute/run entrypoint'}
except Exception as e: except Exception as e:
result = {'success': False, 'error': str(e)} result = {'success': False, 'error': str(e)}
_log_tool_completed('browser', command, bool(result.get('success')), result.get('error'))
payload = {'type': 'browser_control_result', 'id': cmd_id, 'command': command} payload = {'type': 'browser_control_result', 'id': cmd_id, 'command': command}
if isinstance(result, dict):
payload.update(result) payload.update(result)
await self._send_json(ws, payload)
async def _handle_audio_control(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
_log_audio_received(action)
if self.audio is None:
await self._send_json(ws, {
'type': 'audio_control_result',
'id': cmd_id,
'action': action,
'success': False,
'error': 'audio_control not available on this node',
})
return
try:
if action == 'list_audio_devices':
result = self.audio.list_audio_devices()
elif action == 'get_audio_status':
result = self.audio.get_audio_status()
elif action == 'capture_output':
result = self.audio.capture_output(params)
elif action == 'capture_input':
result = self.audio.capture_input(params)
elif action == 'play_audio':
result = self.audio.play_audio(params)
else:
result = {'success': False, 'error': f'Unknown audio_control action: {action}'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('audio', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'audio_control_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload)
async def _handle_camera_control(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
_log_camera_received(action)
if self.camera is None:
await self._send_json(ws, {
'type': 'camera_control_result',
'id': cmd_id,
'action': action,
'success': False,
'error': 'camera_control not available on this node',
})
return
try:
if action == 'list_cameras':
result = self.camera.list_cameras()
elif action == 'get_camera_status':
result = self.camera.get_camera_status()
elif action == 'capture_frame':
result = self.camera.capture_frame(params)
elif action == 'capture_video':
result = self.camera.capture_video(params)
else: else:
payload['result'] = result result = {'success': False, 'error': f'Unknown camera_control action: {action}'}
payload.setdefault('success', True) except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('camera', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'camera_control_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload) await self._send_json(ws, payload)
...@@ -718,9 +1497,8 @@ def main(): ...@@ -718,9 +1497,8 @@ def main():
logger.error("ERROR: Token not set in config. Edit ~/.config/hermes-node/config.json") logger.error("ERROR: Token not set in config. Edit ~/.config/hermes-node/config.json")
sys.exit(1) sys.exit(1)
logger.info(f"Node '{config['node_name']}' starting — tools: {NodeAgent(args.config)._get_available_tools()}")
agent = NodeAgent(args.config) agent = NodeAgent(args.config)
_log_start(config['node_name'], agent._get_available_tools())
try: try:
asyncio.run(agent.connect_and_run()) asyncio.run(agent.connect_and_run())
except KeyboardInterrupt: except KeyboardInterrupt:
......
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/bin/bash #!/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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
...@@ -155,6 +161,14 @@ p = Path(r'''$CONFIG_DIR/config.json''') ...@@ -155,6 +161,14 @@ p = Path(r'''$CONFIG_DIR/config.json''')
data = json.loads(p.read_text()) data = json.loads(p.read_text())
print(str(bool(data.get('enable_audio_control', False))).lower()) print(str(bool(data.get('enable_audio_control', False))).lower())
PY PY
)
EXISTING_ENABLE_CAMERA_CONTROL=$(python3 - <<PY
import json
from pathlib import Path
p = Path(r'''$CONFIG_DIR/config.json''')
data = json.loads(p.read_text())
print(str(bool(data.get('enable_camera_control', False))).lower())
PY
) )
echo " Existing config found: $CONFIG_DIR/config.json" echo " Existing config found: $CONFIG_DIR/config.json"
fi fi
...@@ -204,6 +218,15 @@ case "$ENABLE_AUDIO_INPUT" in ...@@ -204,6 +218,15 @@ case "$ENABLE_AUDIO_INPUT" in
*) ENABLE_AUDIO_CONTROL=false ;; *) ENABLE_AUDIO_CONTROL=false ;;
esac esac
DEFAULT_CAMERA_CHOICE="n"
[ "$EXISTING_ENABLE_CAMERA_CONTROL" = "true" ] && DEFAULT_CAMERA_CHOICE="y"
read -r -p "Enable camera_control? (y/N) [$DEFAULT_CAMERA_CHOICE]: " ENABLE_CAMERA_INPUT
ENABLE_CAMERA_INPUT=${ENABLE_CAMERA_INPUT:-$DEFAULT_CAMERA_CHOICE}
case "$ENABLE_CAMERA_INPUT" in
y|Y|yes|YES) ENABLE_CAMERA_CONTROL=true ;;
*) ENABLE_CAMERA_CONTROL=false ;;
esac
CAPABILITIES='["exec"]' CAPABILITIES='["exec"]'
if [ "$ENABLE_BROWSER" = true ]; then if [ "$ENABLE_BROWSER" = true ]; then
CAPABILITIES=$(python3 - <<'PY' "$CAPABILITIES" CAPABILITIES=$(python3 - <<'PY' "$CAPABILITIES"
...@@ -245,6 +268,16 @@ print(json.dumps(caps)) ...@@ -245,6 +268,16 @@ print(json.dumps(caps))
PY PY
) )
fi fi
if [ "$ENABLE_CAMERA_CONTROL" = true ]; then
CAPABILITIES=$(python3 - <<'PY' "$CAPABILITIES"
import json, sys
caps = json.loads(sys.argv[1])
if 'camera_control' not in caps:
caps.append('camera_control')
print(json.dumps(caps))
PY
)
fi
cat > "$CONFIG_DIR/config.json" << EOF cat > "$CONFIG_DIR/config.json" << EOF
{ {
...@@ -258,6 +291,7 @@ cat > "$CONFIG_DIR/config.json" << EOF ...@@ -258,6 +291,7 @@ cat > "$CONFIG_DIR/config.json" << EOF
"enable_computer_control": $ENABLE_COMPUTER_CONTROL, "enable_computer_control": $ENABLE_COMPUTER_CONTROL,
"enable_desktop_observe": $ENABLE_DESKTOP_OBSERVE, "enable_desktop_observe": $ENABLE_DESKTOP_OBSERVE,
"enable_audio_control": $ENABLE_AUDIO_CONTROL, "enable_audio_control": $ENABLE_AUDIO_CONTROL,
"enable_camera_control": $ENABLE_CAMERA_CONTROL,
"permissions": { "permissions": {
"deny": ["sudo", "su", "doas", "dd if=", "mkfs", "fdisk", "wipe"], "deny": ["sudo", "su", "doas", "dd if=", "mkfs", "fdisk", "wipe"],
"ask": ["rm -rf", "dd if=", "> /dev/", "chmod", "chown", "mv /", ":/usr/", ":/etc/", ":/bin/", ":/sbin/"], "ask": ["rm -rf", "dd if=", "> /dev/", "chmod", "chown", "mv /", ":/usr/", ":/etc/", ":/bin/", ":/sbin/"],
......
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
...@@ -204,7 +207,50 @@ All messages are JSON over WebSocket. ...@@ -204,7 +207,50 @@ All messages are JSON over WebSocket.
### Capability Registration ### Capability Registration
Node registers `browser_control` in capabilities during registration: ## Camera Control
**Optional capability** — phase-1 Linux support uses `ffmpeg` + V4L2 (`/dev/video*`). The node only advertises `camera_control` in its `tools` list when `enable_camera_control` is true *and* a usable camera backend/device is present at runtime.
### Actions
- `list_cameras` — enumerate detected camera devices and probe metadata
- `get_camera_status` — current camera backend readiness and discovered devices
- `capture_frame` — grab a single still frame from a selected/default device
- `capture_video` — record a short video clip from a selected/default device
### Gateway → Node: Camera Control
```json
{
"type": "camera_control",
"id": "camera-a1b2c3d4",
"action": "capture_frame",
"params": {
"device": "/dev/video0",
"format": "png",
"width": 1280,
"height": 720
}
}
```
### Node → Gateway: Camera Control Result
```json
{
"type": "camera_control_result",
"id": "camera-a1b2c3d4",
"action": "capture_frame",
"success": true,
"device": "/dev/video0",
"format": "png",
"path": "/tmp/hermes-camera-1714392000.png",
"size_bytes": 123456,
"data_base64": "..."
}
```
Node registers optional tools in its `tools` list during registration and exposes structured readiness metadata under `capabilities`. Existing siblings include `browser_control`, `computer_control`, `desktop_observe`, `audio_control`, and `camera_control`.
```json ```json
{ {
...@@ -230,7 +276,8 @@ Node registers `browser_control` in capabilities during registration: ...@@ -230,7 +276,8 @@ Node registers `browser_control` in capabilities during registration:
"token": "node-sissy-secret-token-abc123", "token": "node-sissy-secret-token-abc123",
"sexec_path": "/home/openclaw/.openclaw/skills/sexec/sexec.sh", "sexec_path": "/home/openclaw/.openclaw/skills/sexec/sexec.sh",
"reconnect_interval": 5, "reconnect_interval": 5,
"heartbeat_interval": 30 "heartbeat_interval": 30,
"enable_camera_control": false
} }
``` ```
......
<!-- 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 Agent # Hermes Node Agent
**Version:** 2.0 **Version:** 2.0
......
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/usr/bin/env python3 #!/usr/bin/env python3
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/bin/bash #!/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 Agent Installer — Linux Version # Hermes Node Agent Installer — Linux Version
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
...@@ -81,7 +87,7 @@ cd "$TMP_EXTRACT" ...@@ -81,7 +87,7 @@ cd "$TMP_EXTRACT"
# === EMBEDDED TARBALL (base64) === # === EMBEDDED TARBALL (base64) ===
cat <<'TARBALL_DATA' | base64 -d | tar xzf - cat <<'TARBALL_DATA' | base64 -d | tar xzf -
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
TARBALL_DATA TARBALL_DATA
# Copy agent script # Copy agent script
......
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/usr/bin/env python3 #!/usr/bin/env python3
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
...@@ -15,32 +21,21 @@ Author: Lisa (Hermes AI) ...@@ -15,32 +21,21 @@ Author: Lisa (Hermes AI)
Date: 2026-04-30 (enhanced) Date: 2026-04-30 (enhanced)
""" """
import argparse
import asyncio import asyncio
import base64
import json import json
import logging import logging
import os import os
import shutil
import shlex
import ssl import ssl
import subprocess import subprocess
import sys import sys
import time import time
import argparse
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
try:
import websockets
except ImportError:
print("ERROR: websockets library not found. Install with: pip install websockets")
sys.exit(1)
try:
from browser_controller import BrowserController
HAS_BROWSER = True
except ImportError:
HAS_BROWSER = False
logger = logging.getLogger(__name__)
LOG_PREVIEW_LEN = 120 LOG_PREVIEW_LEN = 120
...@@ -77,6 +72,13 @@ def _log_connected() -> None: ...@@ -77,6 +72,13 @@ def _log_connected() -> None:
logger.info("connect ✓") logger.info("connect ✓")
def _log_disconnected(reason: Any = None) -> None:
if reason:
logger.info(f"disconnect — {reason}")
else:
logger.info("disconnect")
def _log_registering(node_name: str) -> None: def _log_registering(node_name: str) -> None:
logger.info(f"register ▶ {node_name}") logger.info(f"register ▶ {node_name}")
...@@ -102,6 +104,12 @@ def _log_exec_failed(command: Any, error: Any, exit_code: Any = None) -> None: ...@@ -102,6 +104,12 @@ def _log_exec_failed(command: Any, error: Any, exit_code: Any = None) -> None:
logger.error(f"{prefix} — {_preview_command(command)} — {error}") logger.error(f"{prefix} — {_preview_command(command)} — {error}")
def _log_tool_completed(tool_name: str, label: Any, success: bool, error: Any = None) -> None:
mark = '✓' if success else '✗'
suffix = f" — {error}" if error else ''
logger.info(f"{tool_name} {mark} {_preview_command(label)}{suffix}")
def _log_browser_received(command: Any) -> None: def _log_browser_received(command: Any) -> None:
logger.info(f"browser ▶ {_preview_command(command)}") logger.info(f"browser ▶ {_preview_command(command)}")
...@@ -110,6 +118,14 @@ def _log_cc_received(action: Any) -> None: ...@@ -110,6 +118,14 @@ def _log_cc_received(action: Any) -> None:
logger.info(f"computer ▶ {_preview_command(action)}") logger.info(f"computer ▶ {_preview_command(action)}")
def _log_audio_received(action: Any) -> None:
logger.info(f"audio ▶ {_preview_command(action)}")
def _log_camera_received(action: Any) -> None:
logger.info(f"camera ▶ {_preview_command(action)}")
def _log_reconnect(delay: Any, reason: Any) -> None: def _log_reconnect(delay: Any, reason: Any) -> None:
logger.info(f"reconnect in {delay}s — {reason}") logger.info(f"reconnect in {delay}s — {reason}")
...@@ -149,6 +165,22 @@ def _log_disabled(component: str, message: str) -> None: ...@@ -149,6 +165,22 @@ def _log_disabled(component: str, message: str) -> None:
def _log_shutdown() -> None: def _log_shutdown() -> None:
logger.info("shutdown") logger.info("shutdown")
logger = logging.getLogger(__name__)
try:
import websockets
except ImportError:
print("ERROR: websockets library not found. Install with: pip install websockets")
sys.exit(1)
try:
from browser_controller import BrowserController
HAS_BROWSER = True
except ImportError:
HAS_BROWSER = False
logger = logging.getLogger(__name__)
# ═════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════
# DEFAULT CONFIGURATION # DEFAULT CONFIGURATION
# ═════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════
...@@ -156,16 +188,18 @@ def _log_shutdown() -> None: ...@@ -156,16 +188,18 @@ def _log_shutdown() -> None:
DEFAULT_GATEWAY_TOKEN = 'GATEWAY_TOKEN_MUST_BE_PROVIDED' DEFAULT_GATEWAY_TOKEN = 'GATEWAY_TOKEN_MUST_BE_PROVIDED'
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
'gateway_url': 'ws://127.0.0.1:8765', 'gateway_url': 'wss://localhost:8765',
'node_name': 'unknown', 'node_name': 'unknown',
'token': DEFAULT_GATEWAY_TOKEN, 'token': DEFAULT_GATEWAY_TOKEN,
'sexec_path': '~/.config/hermes-node/sexec/sexec.sh',
'reconnect_interval': 5, 'reconnect_interval': 5,
'heartbeat_interval': 30, 'heartbeat_interval': 30,
'gateway_cert_path': None, 'gateway_cert_path': None,
'capabilities': ['exec'], 'capabilities': ['exec'],
'enable_browser': False, 'enable_browser': False,
'enable_computer_control': False, 'enable_computer_control': False,
'enable_desktop_observe': False,
'enable_audio_control': False,
'enable_camera_control': False,
} }
# ═════════════════════════════════════════════════════════════════════════ # ═════════════════════════════════════════════════════════════════════════
...@@ -194,21 +228,22 @@ class CommandExecutor: ...@@ -194,21 +228,22 @@ class CommandExecutor:
def __init__(self, permission_rules: Dict[str, List[str]]): def __init__(self, permission_rules: Dict[str, List[str]]):
self.permissions = permission_rules or {'allow': [], 'deny': [], 'ask': []} self.permissions = permission_rules or {'allow': [], 'deny': [], 'ask': []}
def execute(self, command: str, approved: bool = False) -> Dict[str, Any]: def execute(self, command: Any, approved: bool = False) -> Dict[str, Any]:
"""Execute command respecting permission rules.""" """Execute command respecting permission rules."""
raise NotImplementedError raise NotImplementedError
def _normalize_command_text(self, command: Any) -> str:
if isinstance(command, (list, tuple)):
return ' '.join(str(part) for part in command).strip()
return str(command).strip()
def _check_permission(self, command: Any, approved: bool) -> tuple[bool, str]: def _check_permission(self, command: Any, approved: bool) -> tuple[bool, str]:
"""Check allow/deny/ask rules. """Check allow/deny/ask rules.
Returns (allowed, reason). 'ask' means requires approval gate. Returns (allowed, reason). 'ask' means requires approval gate.
Accepts command as string or argv list. Accepts command as string or argv list.
""" """
import re import re
if isinstance(command, (list, tuple)): cmd = self._normalize_command_text(command)
cmd = ' '.join(str(part) for part in command)
else:
cmd = str(command)
cmd = cmd.strip()
# Deny (highest priority) # Deny (highest priority)
for pattern in self.permissions.get('deny', []): for pattern in self.permissions.get('deny', []):
if re.search(pattern, cmd, re.IGNORECASE): if re.search(pattern, cmd, re.IGNORECASE):
...@@ -238,7 +273,7 @@ class PosixCommandExecutor(CommandExecutor): ...@@ -238,7 +273,7 @@ class PosixCommandExecutor(CommandExecutor):
if not approved and reason == 'ask': if not approved and reason == 'ask':
return {'success': False, 'error': 'Command requires approval', 'exit_code': 2} return {'success': False, 'error': 'Command requires approval', 'exit_code': 2}
if isinstance(command, (list, tuple)): if isinstance(command, (list, tuple)):
cmd = ' '.join(subprocess.list2cmdline([str(part)]) if is_windows() else __import__('shlex').quote(str(part)) for part in command) cmd = ' '.join(shlex.quote(str(part)) for part in command)
else: else:
cmd = str(command) cmd = str(command)
try: try:
...@@ -505,13 +540,290 @@ class WindowsComputerController(ComputerControllerBase): ...@@ -505,13 +540,290 @@ class WindowsComputerController(ComputerControllerBase):
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
# ── AUDIO CONTROL ──────────────────────────────────────────────────────────
class AudioControllerBase:
"""Base class for audio device/media actions."""
def capability_info(self) -> Dict[str, Any]:
raise NotImplementedError
def list_audio_devices(self) -> Dict[str, Any]:
raise NotImplementedError
def get_audio_status(self) -> Dict[str, Any]:
raise NotImplementedError
def capture_output(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
def capture_input(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
def play_audio(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
class CameraControllerBase:
"""Base class for camera device/media actions."""
def capability_info(self) -> Dict[str, Any]:
raise NotImplementedError
def list_cameras(self) -> Dict[str, Any]:
raise NotImplementedError
def get_camera_status(self) -> Dict[str, Any]:
raise NotImplementedError
def capture_frame(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
def capture_video(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
class PosixAudioController(AudioControllerBase):
"""Linux audio support via ffmpeg + available host backends."""
def __init__(self):
self.ffmpeg = shutil.which('ffmpeg')
if not self.ffmpeg:
raise PlatformError('ffmpeg not found')
self.ffplay = shutil.which('ffplay')
self.ffprobe = shutil.which('ffprobe')
self.pactl = shutil.which('pactl')
self.arecord = shutil.which('arecord')
self.aplay = shutil.which('aplay')
self.backend = self._detect_backend()
def _detect_backend(self) -> str:
if self.pactl:
probe = self._run_quiet([self.pactl, 'info'])
if probe['success']:
server = (probe.get('stdout') or '').lower()
if 'pipewire' in server:
return 'pipewire-pulse'
return 'pulseaudio'
if os.environ.get('PIPEWIRE_RUNTIME_DIR') or os.environ.get('XDG_RUNTIME_DIR'):
return 'pipewire'
if self.arecord:
return 'alsa'
return 'unknown'
def capability_info(self) -> Dict[str, Any]:
monitor_ready, monitor_name = self._default_monitor_source()
input_ready, input_source = self._default_input_source()
return {
'platform': 'linux',
'backend': self.backend,
'available': True,
'can_capture_output': monitor_ready,
'can_capture_input': input_ready,
'can_play_audio': bool(self.ffplay or self.aplay or self.ffmpeg),
'can_inject_mic': False,
'capture_output_ready': monitor_ready,
'capture_output_backend': 'pulseaudio-monitor' if monitor_ready else None,
'default_output_monitor': monitor_name,
'default_input_source': input_source,
'ffmpeg': bool(self.ffmpeg),
'ffplay': bool(self.ffplay),
'pactl': bool(self.pactl),
'arecord': bool(self.arecord),
'aplay': bool(self.aplay),
}
def _run_quiet(self, cmd: List[str], timeout: int = 15) -> Dict[str, Any]:
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return {
'success': proc.returncode == 0,
'stdout': proc.stdout,
'stderr': proc.stderr,
'exit_code': proc.returncode,
}
except Exception as e:
return {'success': False, 'stdout': '', 'stderr': str(e), 'exit_code': -1}
def _default_output_sink(self) -> Optional[str]:
if not self.pactl:
return None
result = self._run_quiet([self.pactl, 'get-default-sink'])
sink = (result.get('stdout') or '').strip()
if result['success'] and sink:
return sink
return None
def _default_input_source(self) -> tuple[bool, Optional[str]]:
if self.pactl:
result = self._run_quiet([self.pactl, 'get-default-source'])
source = (result.get('stdout') or '').strip()
if result['success'] and source:
return True, source
if self.arecord:
return True, 'default'
return False, None
def _default_monitor_source(self) -> tuple[bool, Optional[str]]:
sink = self._default_output_sink()
if sink:
return True, f'{sink}.monitor'
return False, None
def _expand_output_path(self, path: Optional[str], suffix: str) -> str:
if path:
return str(Path(path).expanduser())
stamp = int(time.time())
return f'/tmp/hermes-audio-{stamp}{suffix}'
def _encode_file(self, path: str) -> Dict[str, Any]:
data = Path(path).read_bytes()
return {
'path': path,
'size_bytes': len(data),
'data_base64': base64.b64encode(data).decode('ascii'),
}
def _media_duration(self, path: str) -> Optional[float]:
if not self.ffprobe:
return None
result = self._run_quiet([
self.ffprobe, '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', path
])
if not result['success']:
return None
try:
return round(float((result.get('stdout') or '').strip()), 3)
except Exception:
return None
def list_audio_devices(self) -> Dict[str, Any]:
devices: Dict[str, Any] = {'backend': self.backend, 'sinks': [], 'sources': []}
if self.pactl:
sink_res = self._run_quiet([self.pactl, 'list', 'short', 'sinks'])
source_res = self._run_quiet([self.pactl, 'list', 'short', 'sources'])
if sink_res['success']:
for line in sink_res.get('stdout', '').splitlines():
parts = line.split('\t')
if len(parts) >= 2:
devices['sinks'].append({'id': parts[0], 'name': parts[1], 'raw': line})
if source_res['success']:
for line in source_res.get('stdout', '').splitlines():
parts = line.split('\t')
if len(parts) >= 2:
devices['sources'].append({'id': parts[0], 'name': parts[1], 'raw': line})
if not devices['sources'] and self.arecord:
src = self._run_quiet([self.arecord, '-l'])
devices['sources_raw'] = src.get('stdout', '')
return {'success': True, **devices}
def get_audio_status(self) -> Dict[str, Any]:
monitor_ready, monitor_name = self._default_monitor_source()
input_ready, input_source = self._default_input_source()
status = {
'success': True,
'backend': self.backend,
'default_output_sink': self._default_output_sink(),
'default_output_monitor': monitor_name,
'default_input_source': input_source,
'capture_output_ready': monitor_ready,
'capture_input_ready': input_ready,
'can_play_audio': bool(self.ffplay or self.aplay or self.ffmpeg),
}
if self.pactl:
info = self._run_quiet([self.pactl, 'info'])
if info['success']:
status['server_info'] = info.get('stdout', '')
return status
def capture_output(self, params: Dict[str, Any]) -> Dict[str, Any]:
duration = max(1, min(int(params.get('duration', 5)), 600))
fmt = str(params.get('format', 'wav')).lower()
path = self._expand_output_path(params.get('output_path') or params.get('path'), f'.{fmt}')
monitor_ready, monitor = self._default_monitor_source()
if not monitor_ready or not monitor:
return {'success': False, 'error': 'No PulseAudio/PipeWire monitor source available for output capture'}
cmd = [
self.ffmpeg, '-y', '-v', 'error', '-f', 'pulse', '-i', monitor,
'-t', str(duration), path,
]
result = self._run_quiet(cmd, timeout=duration + 15)
if not result['success']:
return {'success': False, 'error': (result.get('stderr') or result.get('stdout') or 'ffmpeg capture failed').strip()}
payload = {
'success': True,
'format': fmt,
'duration': duration,
'source': monitor,
}
payload.update(self._encode_file(path))
media_duration = self._media_duration(path)
if media_duration is not None:
payload['measured_duration'] = media_duration
return payload
def capture_input(self, params: Dict[str, Any]) -> Dict[str, Any]:
duration = max(1, min(int(params.get('duration', 5)), 600))
fmt = str(params.get('format', 'wav')).lower()
path = self._expand_output_path(params.get('output_path') or params.get('path'), f'.{fmt}')
source = params.get('source')
input_ready, default_source = self._default_input_source()
if not source:
source = default_source
if self.pactl and source:
cmd = [
self.ffmpeg, '-y', '-v', 'error', '-f', 'pulse', '-i', str(source),
'-t', str(duration), path,
]
result = self._run_quiet(cmd, timeout=duration + 15)
elif self.arecord and input_ready:
cmd = [self.arecord, '-q', '-d', str(duration), path]
result = self._run_quiet(cmd, timeout=duration + 15)
else:
return {'success': False, 'error': 'No usable input capture backend available'}
if not result['success']:
return {'success': False, 'error': (result.get('stderr') or result.get('stdout') or 'input capture failed').strip()}
payload = {
'success': True,
'format': fmt,
'duration': duration,
'source': source,
}
payload.update(self._encode_file(path))
media_duration = self._media_duration(path)
if media_duration is not None:
payload['measured_duration'] = media_duration
return payload
def play_audio(self, params: Dict[str, Any]) -> Dict[str, Any]:
path = params.get('path')
if not path:
return {'success': False, 'error': 'play_audio requires params.path'}
path = str(Path(path).expanduser())
if not Path(path).exists():
return {'success': False, 'error': f'Audio file not found: {path}'}
if self.ffplay:
cmd = [self.ffplay, '-nodisp', '-autoexit', '-loglevel', 'error', path]
elif self.aplay:
cmd = [self.aplay, path]
else:
cmd = [self.ffmpeg, '-v', 'error', '-i', path, '-f', 'null', '-']
result = self._run_quiet(cmd, timeout=max(30, int(params.get('timeout', 120))))
if not result['success']:
return {'success': False, 'error': (result.get('stderr') or result.get('stdout') or 'audio playback failed').strip()}
payload = {'success': True, 'path': path}
media_duration = self._media_duration(path)
if media_duration is not None:
payload['duration'] = media_duration
return payload
# ── Factory functions ───────────────────────────────────────────────────── # ── Factory functions ─────────────────────────────────────────────────────
def make_executor(config: Dict[str, Any]) -> CommandExecutor: def make_executor(config: Dict[str, Any]) -> CommandExecutor:
"""Select appropriate command executor for current platform.""" """Select appropriate command executor for current platform."""
perms = config.get('permissions', {}) perms = config.get('permissions', {})
if is_windows(): if is_windows():
return WindowsCommandExecutor(perms) return WindowsCommandExecutor(perms)
else:
return PosixCommandExecutor(perms) return PosixCommandExecutor(perms)
def make_computer_controller(config: Dict[str, Any]) -> Optional[ComputerControllerBase]: def make_computer_controller(config: Dict[str, Any]) -> Optional[ComputerControllerBase]:
...@@ -522,21 +834,262 @@ def make_computer_controller(config: Dict[str, Any]) -> Optional[ComputerControl ...@@ -522,21 +834,262 @@ def make_computer_controller(config: Dict[str, Any]) -> Optional[ComputerControl
try: try:
return WindowsComputerController() return WindowsComputerController()
except ImportError as e: except ImportError as e:
_log_disabled('computer_control', f"missing deps: {e}") logger.warning(f"computer_control disabled (missing deps): {e}")
return None return None
else: else:
# Linux/macOS # Linux/macOS
if is_macos(): if is_macos():
_log_disabled('computer_control', 'macOS not implemented yet') logger.warning("macOS computer_control not implemented yet")
return None return None
# Linux # Linux
if subprocess.run(['which', 'xdotool'], capture_output=True).returncode != 0: if subprocess.run(['which', 'xdotool'], capture_output=True).returncode != 0:
_log_disabled('computer_control', 'xdotool not found') logger.warning("xdotool not found — computer_control disabled")
return None return None
try: try:
return PosixComputerController() return PosixComputerController()
except Exception as e: except Exception as e:
_log_init_warning('computer_control', e) logger.warning(f"computer_control init failed: {e}")
return None
def make_audio_controller(config: Dict[str, Any]) -> Optional[AudioControllerBase]:
if not config.get('enable_audio_control'):
return None
if is_linux():
try:
return PosixAudioController()
except Exception as e:
_log_disabled('audio_control', str(e))
return None
_log_disabled('audio_control', f'unsupported platform: {sys.platform}')
return None
class PosixCameraController(CameraControllerBase):
"""Linux camera support via ffmpeg + V4L2 device nodes."""
def __init__(self):
self.ffmpeg = shutil.which('ffmpeg')
if not self.ffmpeg:
raise PlatformError('ffmpeg not found')
self.ffprobe = shutil.which('ffprobe')
self.v4l2_ctl = shutil.which('v4l2-ctl')
def _list_device_paths(self) -> List[Path]:
return sorted(Path('/dev').glob('video*'), key=lambda p: p.name)
def _encode_file(self, path: str) -> Dict[str, Any]:
data = Path(path).read_bytes()
return {
'path': path,
'size_bytes': len(data),
'data_base64': base64.b64encode(data).decode('ascii'),
}
def _media_duration(self, path: str) -> Optional[float]:
if not self.ffprobe:
return None
try:
proc = subprocess.run([
self.ffprobe, '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', path
], capture_output=True, text=True, timeout=15)
if proc.returncode != 0:
return None
return round(float((proc.stdout or '').strip()), 3)
except Exception:
return None
def _expand_output_path(self, path: Optional[str], suffix: str) -> str:
if path:
return str(Path(path).expanduser())
stamp = int(time.time())
return f'/tmp/hermes-camera-{stamp}{suffix}'
def _ffmpeg_probe(self, device: str) -> Dict[str, Any]:
try:
proc = subprocess.run(
[self.ffmpeg, '-hide_banner', '-f', 'v4l2', '-list_formats', 'all', '-i', device],
capture_output=True,
text=True,
timeout=15,
)
text = '\n'.join(part for part in [proc.stdout, proc.stderr] if part)
return {
'success': proc.returncode in (0, 1),
'output': text.strip(),
'exit_code': proc.returncode,
}
except Exception as e:
return {'success': False, 'output': str(e), 'exit_code': -1}
def _device_info(self, device_path: Path) -> Dict[str, Any]:
info = {
'path': str(device_path),
'name': device_path.name,
'exists': device_path.exists(),
'readable': os.access(device_path, os.R_OK),
'writable': os.access(device_path, os.W_OK),
}
by_id_root = Path('/dev/v4l/by-id')
aliases = []
if by_id_root.exists():
for alias in sorted(by_id_root.iterdir()):
try:
if alias.resolve() == device_path.resolve():
aliases.append(str(alias))
except Exception:
continue
if aliases:
info['aliases'] = aliases
if self.v4l2_ctl:
try:
proc = subprocess.run(
[self.v4l2_ctl, '--device', str(device_path), '--all'],
capture_output=True,
text=True,
timeout=15,
)
info['details'] = (proc.stdout or proc.stderr or '').strip()
info['available'] = proc.returncode == 0
except Exception as e:
info['available'] = False
info['probe_error'] = str(e)
else:
probe = self._ffmpeg_probe(str(device_path))
info['available'] = probe['success']
if probe.get('output'):
info['details'] = probe['output']
return info
def capability_info(self) -> Dict[str, Any]:
devices = self._list_device_paths()
return {
'platform': 'linux',
'backend': 'v4l2-ffmpeg',
'available': bool(devices),
'device_count': len(devices),
'supports_frame_capture': True,
'supports_video_capture': True,
'ffmpeg': bool(self.ffmpeg),
'ffprobe': bool(self.ffprobe),
'v4l2_ctl': bool(self.v4l2_ctl),
'devices': [str(p) for p in devices],
}
def list_cameras(self) -> Dict[str, Any]:
devices = [self._device_info(path) for path in self._list_device_paths()]
return {
'success': True,
'backend': 'v4l2-ffmpeg',
'camera_count': len(devices),
'cameras': devices,
}
def get_camera_status(self) -> Dict[str, Any]:
devices = self.list_cameras()
payload = {
'success': True,
'backend': 'v4l2-ffmpeg',
'ffmpeg': bool(self.ffmpeg),
'ffprobe': bool(self.ffprobe),
'v4l2_ctl': bool(self.v4l2_ctl),
'camera_count': devices.get('camera_count', 0),
'cameras': devices.get('cameras', []),
}
if payload['camera_count'] == 0:
payload['available'] = False
payload['reason'] = 'No /dev/video* devices found'
else:
payload['available'] = True
return payload
def _pick_device(self, params: Dict[str, Any]) -> str:
device = params.get('device') or params.get('device_path')
if device:
return str(Path(str(device)).expanduser())
devices = self._list_device_paths()
if not devices:
raise PlatformError('No /dev/video* devices found')
return str(devices[0])
def capture_frame(self, params: Dict[str, Any]) -> Dict[str, Any]:
device = self._pick_device(params)
fmt = str(params.get('format', 'png')).lower()
if fmt not in ('png', 'jpg', 'jpeg', 'bmp'):
return {'success': False, 'error': f'Unsupported frame format: {fmt}'}
suffix = '.jpg' if fmt == 'jpeg' else f'.{fmt}'
path = self._expand_output_path(params.get('output_path') or params.get('path'), suffix)
width = params.get('width')
height = params.get('height')
cmd = [self.ffmpeg, '-y', '-v', 'error', '-f', 'v4l2']
if width and height:
cmd += ['-video_size', f'{int(width)}x{int(height)}']
cmd += ['-i', device, '-frames:v', '1', path]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
except Exception as e:
return {'success': False, 'error': str(e)}
if proc.returncode != 0:
return {'success': False, 'error': (proc.stderr or proc.stdout or 'frame capture failed').strip()}
payload = {
'success': True,
'device': device,
'format': fmt,
}
payload.update(self._encode_file(path))
return payload
def capture_video(self, params: Dict[str, Any]) -> Dict[str, Any]:
device = self._pick_device(params)
duration = max(1, min(int(params.get('duration', 5)), 600))
fmt = str(params.get('format', 'mp4')).lower()
extension = 'mkv' if fmt == 'matroska' else fmt
if extension not in ('mp4', 'mkv', 'webm'):
return {'success': False, 'error': f'Unsupported video format: {fmt}'}
path = self._expand_output_path(params.get('output_path') or params.get('path'), f'.{extension}')
width = params.get('width')
height = params.get('height')
fps = params.get('fps')
cmd = [self.ffmpeg, '-y', '-v', 'error', '-f', 'v4l2']
if fps:
cmd += ['-framerate', str(fps)]
if width and height:
cmd += ['-video_size', f'{int(width)}x{int(height)}']
cmd += ['-i', device, '-t', str(duration), path]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=duration + 30)
except Exception as e:
return {'success': False, 'error': str(e)}
if proc.returncode != 0:
return {'success': False, 'error': (proc.stderr or proc.stdout or 'video capture failed').strip()}
payload = {
'success': True,
'device': device,
'format': extension,
'duration': duration,
}
payload.update(self._encode_file(path))
media_duration = self._media_duration(path)
if media_duration is not None:
payload['measured_duration'] = media_duration
return payload
def make_camera_controller(config: Dict[str, Any]) -> Optional[CameraControllerBase]:
if not config.get('enable_camera_control'):
return None
if is_linux():
try:
controller = PosixCameraController()
if not controller._list_device_paths():
_log_disabled('camera_control', 'no /dev/video* devices found')
return None
return controller
except Exception as e:
_log_disabled('camera_control', str(e))
return None
_log_disabled('camera_control', f'unsupported platform: {sys.platform}')
return None return None
...@@ -545,26 +1098,34 @@ class NodeAgent: ...@@ -545,26 +1098,34 @@ class NodeAgent:
self.config = self._load_config(config_path) self.config = self._load_config(config_path)
self.executor = make_executor(self.config) self.executor = make_executor(self.config)
self.computer = make_computer_controller(self.config) self.computer = make_computer_controller(self.config)
self.audio = make_audio_controller(self.config)
self.camera = make_camera_controller(self.config)
self.browser = None self.browser = None
if self.config.get('enable_browser') and HAS_BROWSER: if self.config.get('enable_browser') and HAS_BROWSER:
try: try:
self.browser = BrowserController() self.browser = BrowserController()
except Exception as e: except Exception as e:
_log_init_warning('browser_control', e) logger.warning(f"BrowserController init failed: {e}")
self.capabilities = self._detect_capabilities() self.capabilities = self._detect_capabilities()
def _load_config(self, path: Optional[str]) -> Dict[str, Any]: def _load_config(self, path: Optional[str]) -> Dict[str, Any]:
"""Load node configuration from JSON.""" """Load node configuration from JSON."""
cfg_path = Path(path).expanduser() if path else Path.home() / '.config' / 'hermes-node' / 'config.json' cfg_path = Path(path).expanduser() if path else Path.home() / '.config' / 'hermes-node' / 'config.json'
if not cfg_path.exists(): if not cfg_path.exists():
_log_config_missing(cfg_path) logger.error(f"Config not found: {cfg_path}")
logger.error(f"Run the installer or copy config-template.json to {cfg_path}")
sys.exit(1) sys.exit(1)
try:
with open(cfg_path) as f: with open(cfg_path) as f:
data = json.load(f) data = json.load(f)
except json.JSONDecodeError as e:
logger.error(f"Config file is not valid JSON: {e}")
logger.error(f"File: {cfg_path}")
sys.exit(1)
# Merge defaults # Merge defaults
merged = {**DEFAULT_CONFIG, **data} merged = {**DEFAULT_CONFIG, **data}
if not merged['token']: if not merged['token']:
_log_token_missing() logger.error("Token missing from config")
sys.exit(1) sys.exit(1)
return merged return merged
...@@ -573,10 +1134,13 @@ class NodeAgent: ...@@ -573,10 +1134,13 @@ class NodeAgent:
caps = { caps = {
'enable_browser': self.config.get('enable_browser', False), 'enable_browser': self.config.get('enable_browser', False),
'enable_computer_control': self.config.get('enable_computer_control', False), 'enable_computer_control': self.config.get('enable_computer_control', False),
'enable_desktop_observe': self.config.get('enable_desktop_observe', False),
'enable_audio_control': self.config.get('enable_audio_control', False),
'enable_camera_control': self.config.get('enable_camera_control', False),
} }
# Browser detection: extension installed signals availability via separate channel if self.browser is not None:
# Computer control: check system tools caps['browser_control'] = {'available': True}
if caps['enable_computer_control']: if self.computer is not None:
cc_info = { cc_info = {
'display': os.environ.get('DISPLAY', ':0'), 'display': os.environ.get('DISPLAY', ':0'),
'has_xdotool': subprocess.run(['which', 'xdotool'], capture_output=True).returncode == 0, 'has_xdotool': subprocess.run(['which', 'xdotool'], capture_output=True).returncode == 0,
...@@ -584,6 +1148,27 @@ class NodeAgent: ...@@ -584,6 +1148,27 @@ class NodeAgent:
'has_scrot': subprocess.run(['which', 'scrot'], capture_output=True).returncode == 0, 'has_scrot': subprocess.run(['which', 'scrot'], capture_output=True).returncode == 0,
} }
caps['computer_control'] = cc_info caps['computer_control'] = cc_info
if caps['enable_desktop_observe']:
caps['desktop_observe'] = {
'available': self.computer is not None,
'display': os.environ.get('DISPLAY', ':0')
}
if caps['enable_audio_control']:
if self.audio is not None:
caps['audio_control'] = self.audio.capability_info()
else:
caps['audio_control'] = {
'available': False,
'reason': 'audio control requested but backend dependencies are unavailable'
}
if caps['enable_camera_control']:
if self.camera is not None:
caps['camera_control'] = self.camera.capability_info()
else:
caps['camera_control'] = {
'available': False,
'reason': 'camera control requested but no supported camera backend/devices are available'
}
return caps return caps
async def connect_and_run(self): async def connect_and_run(self):
...@@ -592,32 +1177,47 @@ class NodeAgent: ...@@ -592,32 +1177,47 @@ class NodeAgent:
_log_connect(url) _log_connect(url)
ssl_context = None ssl_context = None
if self.config['gateway_url'].startswith('wss://'): if url.startswith('wss://'):
cert_path = self.config.get('gateway_cert_path')
if cert_path:
ssl_context = ssl.create_default_context(cafile=str(Path(cert_path).expanduser()))
else:
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE ssl_context.verify_mode = ssl.CERT_NONE
_log_tls_disabled() _log_tls_disabled()
while True: while True:
try: try:
async with websockets.connect(url, ping_interval=20, ping_timeout=10, ssl=ssl_context) as ws: async with websockets.connect(url, ping_interval=20, ping_timeout=10, ssl=ssl_context) as ws:
_log_connected() _log_connected()
# Send registration expected by gateway # Send registration frame expected by the gateway
_log_registering(self.config['node_name']) _log_registering(self.config['node_name'])
await ws.send(json.dumps({ await ws.send(json.dumps({
"type": "register", "type": "register",
"node_name": self.config['node_name'], "node_name": self.config['node_name'],
"version": "node-agent-clean", "version": "1.0",
"tools": self._get_available_tools(), "tools": self._get_available_tools(),
"capabilities": self.capabilities "capabilities": self.capabilities
})) }))
_log_waiting() _log_waiting()
# Listen for commands heartbeat_task = asyncio.create_task(self._heartbeat_loop(ws))
disconnect_reason = None
try:
async for raw in ws: async for raw in ws:
msg = json.loads(raw) msg = json.loads(raw)
await self._handle_message(ws, msg) await self._handle_message(ws, msg)
except Exception as e:
disconnect_reason = e
raise
finally:
heartbeat_task.cancel()
try:
await heartbeat_task
except asyncio.CancelledError:
pass
_log_disconnected(disconnect_reason)
except Exception as e: except Exception as e:
_log_connection_error(e) _log_connection_error(e)
...@@ -626,11 +1226,17 @@ class NodeAgent: ...@@ -626,11 +1226,17 @@ class NodeAgent:
def _get_available_tools(self) -> list: def _get_available_tools(self) -> list:
"""Return list of capability strings for gateway registration.""" """Return list of capability strings for gateway registration."""
tools = ['exec'] # always present tools = ['exec']
if self.capabilities.get('enable_browser'): if self.browser is not None:
tools.append('browser_control') tools.append('browser_control')
if self.capabilities.get('enable_computer_control'): if self.computer is not None:
tools.append('computer_control') tools.append('computer_control')
if self.config.get('enable_desktop_observe') and self.computer is not None:
tools.append('desktop_observe')
if self.config.get('enable_audio_control') and self.audio is not None:
tools.append('audio_control')
if self.config.get('enable_camera_control') and self.camera is not None:
tools.append('camera_control')
return tools return tools
async def _handle_message(self, ws, msg: Dict[str, Any]): async def _handle_message(self, ws, msg: Dict[str, Any]):
...@@ -638,51 +1244,242 @@ class NodeAgent: ...@@ -638,51 +1244,242 @@ class NodeAgent:
if req_type == 'exec': if req_type == 'exec':
cmd_id = msg['id'] cmd_id = msg['id']
command = msg['command'] command = msg['command']
await self._handle_exec(ws, cmd_id, command) await self._handle_exec(ws, cmd_id, command, msg.get('approved', False))
elif req_type == 'computer_control': elif req_type == 'computer_control':
action = msg['action'] action = msg['action']
params = msg.get('params', {}) params = msg.get('params', {})
await self._handle_cc(ws, msg.get('id'), action, params) await self._handle_cc(ws, msg.get('id'), action, params)
elif req_type == 'desktop_observe':
action = msg['action']
params = msg.get('params', {})
await self._handle_desktop_observe(ws, msg.get('id'), action, params)
elif req_type == 'browser_control':
command = msg.get('command')
params = msg.get('params', {})
await self._handle_browser_control(ws, msg.get('id'), command, params)
elif req_type == 'audio_control':
action = msg['action']
params = msg.get('params', {})
await self._handle_audio_control(ws, msg.get('id'), action, params)
elif req_type == 'camera_control':
action = msg['action']
params = msg.get('params', {})
await self._handle_camera_control(ws, msg.get('id'), action, params)
elif req_type == 'register_ack': elif req_type == 'register_ack':
_log_registered(self.config['node_name'])
_log_registration_ack() _log_registration_ack()
_log_registered(self.config['node_name'])
elif req_type == 'heartbeat_ack': elif req_type == 'heartbeat_ack':
_log_heartbeat_ack() _log_heartbeat_ack()
return
else: else:
_log_unknown_message(req_type) _log_unknown_message(req_type)
async def _handle_exec(self, ws, cmd_id: str, command: str): async def _heartbeat_loop(self, ws):
"""Execute a shell command and reply with the gateway's exec protocol.""" """Send periodic heartbeats so the gateway can track liveness."""
interval = max(5, int(self.config.get('heartbeat_interval', 30)))
try:
while True:
await asyncio.sleep(interval)
await ws.send(json.dumps({
'type': 'heartbeat',
'node_name': self.config['node_name'],
'timestamp': time.time(),
}))
except asyncio.CancelledError:
raise
except Exception as e:
_log_connection_error(f"heartbeat loop stopped: {e}")
async def _send_json(self, ws, payload: Dict[str, Any]):
await ws.send(json.dumps(payload))
async def _handle_exec(self, ws, cmd_id: str, command: str, approved: bool = False):
"""Execute a shell command via the configured executor.
Uses the gateway's current exec protocol:
- optional streamed chunks via ``exec_output``
- terminal result via ``exec_complete``
"""
_log_exec_received(command) _log_exec_received(command)
result = self.executor.execute(command) try:
result = self.executor.execute(command, approved=approved)
stdout = result.get('stdout', '') or '' stdout = result.get('stdout', '') or ''
stderr = result.get('stderr', '') or '' stderr = result.get('stderr', '') or ''
if stdout: if stdout:
await ws.send(json.dumps({ await self._send_json(ws, {
"type": "exec_output", 'type': 'exec_output',
"id": cmd_id, 'id': cmd_id,
"stream": "stdout", 'stream': 'stdout',
"data": stdout 'data': stdout,
})) })
if stderr: if stderr:
await ws.send(json.dumps({ await self._send_json(ws, {
"type": "exec_output", 'type': 'exec_output',
"id": cmd_id, 'id': cmd_id,
"stream": "stderr", 'stream': 'stderr',
"data": stderr 'data': stderr,
})) })
exit_code = result.get('exit_code', -1) exit_code = result.get('exit_code', -1)
error = result.get('error') error = result.get('error')
if error: if error:
_log_exec_failed(command, error, exit_code) _log_exec_failed(command, error, exit_code)
else: else:
_log_exec_completed(command, exit_code) _log_exec_completed(command, exit_code)
await ws.send(json.dumps({ await self._send_json(ws, {
"type": "exec_complete", 'type': 'exec_complete',
"id": cmd_id, 'id': cmd_id,
"exit_code": exit_code, 'exit_code': exit_code,
"error": error 'error': error,
})) })
except Exception as e:
_log_exec_failed(command, str(e), -1)
await self._send_json(ws, {
'type': 'exec_complete',
'id': cmd_id,
'exit_code': -1,
'error': str(e),
})
async def _handle_cc(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
_log_cc_received(action)
if self.computer is None:
result = {
'success': False,
'error': 'computer_control not available on this node',
}
else:
try:
if action == 'screenshot':
result = self.computer.screenshot(params.get('output_path'))
elif action == 'mouse_move':
result = self.computer.mouse_move(int(params['x']), int(params['y']))
elif action == 'mouse_click':
result = self.computer.mouse_click(int(params.get('button', 1)))
elif action == 'mouse_position':
result = self.computer.mouse_position()
elif action == 'type_text':
result = self.computer.type_text(params['text'])
elif action == 'key_press':
result = self.computer.key_press(params['key'])
elif action == 'get_active_window':
result = self.computer.get_active_window()
else:
result = {'success': False, 'error': f'Unknown computer_control action: {action}'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('computer', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'computer_control_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload)
async def _handle_desktop_observe(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
logger.info(f"observe ▶ {_preview_command(action)}")
if self.computer is None:
result = {
'success': False,
'error': 'desktop_observe requires computer_control support on this node',
}
else:
try:
if action in ('active_window', 'get_active_window'):
result = self.computer.get_active_window()
elif action == 'mouse_position':
result = self.computer.mouse_position()
elif action == 'screenshot':
result = self.computer.screenshot(params.get('output_path'))
else:
result = {'success': False, 'error': f'Unknown desktop_observe action: {action}'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('observe', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'desktop_observe_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload)
async def _handle_browser_control(self, ws, cmd_id: str, command: str, params: Dict[str, Any]):
if self.browser is None:
await self._send_json(ws, {
'type': 'browser_control_result',
'id': cmd_id,
'command': command,
'success': False,
'error': 'browser_control not available on this node',
})
return
_log_browser_received(command)
try:
if hasattr(self.browser, 'execute'):
result = self.browser.execute(command, params)
elif hasattr(self.browser, 'run'):
result = self.browser.run(command, params)
else:
result = {'success': False, 'error': 'BrowserController has no execute/run entrypoint'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('browser', command, bool(result.get('success')), result.get('error'))
payload = {'type': 'browser_control_result', 'id': cmd_id, 'command': command}
payload.update(result)
await self._send_json(ws, payload)
async def _handle_audio_control(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
_log_audio_received(action)
if self.audio is None:
await self._send_json(ws, {
'type': 'audio_control_result',
'id': cmd_id,
'action': action,
'success': False,
'error': 'audio_control not available on this node',
})
return
try:
if action == 'list_audio_devices':
result = self.audio.list_audio_devices()
elif action == 'get_audio_status':
result = self.audio.get_audio_status()
elif action == 'capture_output':
result = self.audio.capture_output(params)
elif action == 'capture_input':
result = self.audio.capture_input(params)
elif action == 'play_audio':
result = self.audio.play_audio(params)
else:
result = {'success': False, 'error': f'Unknown audio_control action: {action}'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('audio', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'audio_control_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload)
async def _handle_camera_control(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
_log_camera_received(action)
if self.camera is None:
await self._send_json(ws, {
'type': 'camera_control_result',
'id': cmd_id,
'action': action,
'success': False,
'error': 'camera_control not available on this node',
})
return
try:
if action == 'list_cameras':
result = self.camera.list_cameras()
elif action == 'get_camera_status':
result = self.camera.get_camera_status()
elif action == 'capture_frame':
result = self.camera.capture_frame(params)
elif action == 'capture_video':
result = self.camera.capture_video(params)
else:
result = {'success': False, 'error': f'Unknown camera_control action: {action}'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('camera', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'camera_control_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload)
def main(): def main():
...@@ -691,22 +1488,21 @@ def main(): ...@@ -691,22 +1488,21 @@ def main():
parser.add_argument('--debug', action='store_true', help='Debug logging') parser.add_argument('--debug', action='store_true', help='Debug logging')
args = parser.parse_args() args = parser.parse_args()
_setup_logging(args.debug) if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
# Load config to check token # Load config to check token
config = NodeAgent(args.config)._load_config(args.config) config = NodeAgent(args.config)._load_config(args.config)
if config['token'] == DEFAULT_GATEWAY_TOKEN or config['token'] == 'GATEWAY_TOKEN_MUST_BE_PROVIDED': if config['token'] == DEFAULT_GATEWAY_TOKEN or config['token'] == 'GATEWAY_TOKEN_MUST_BE_PROVIDED':
_log_token_missing() logger.error("ERROR: Token not set in config. Edit ~/.config/hermes-node/config.json")
sys.exit(1) sys.exit(1)
tools = NodeAgent(args.config)._get_available_tools()
_log_start(config['node_name'], tools)
agent = NodeAgent(args.config) agent = NodeAgent(args.config)
_log_start(config['node_name'], agent._get_available_tools())
try: try:
asyncio.run(agent.connect_and_run()) asyncio.run(agent.connect_and_run())
except KeyboardInterrupt: except KeyboardInterrupt:
_log_shutdown() logger.info("Shutting down")
if __name__ == '__main__': if __name__ == '__main__':
main() main()
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/bin/bash #!/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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
...@@ -155,6 +161,14 @@ p = Path(r'''$CONFIG_DIR/config.json''') ...@@ -155,6 +161,14 @@ p = Path(r'''$CONFIG_DIR/config.json''')
data = json.loads(p.read_text()) data = json.loads(p.read_text())
print(str(bool(data.get('enable_audio_control', False))).lower()) print(str(bool(data.get('enable_audio_control', False))).lower())
PY PY
)
EXISTING_ENABLE_CAMERA_CONTROL=$(python3 - <<PY
import json
from pathlib import Path
p = Path(r'''$CONFIG_DIR/config.json''')
data = json.loads(p.read_text())
print(str(bool(data.get('enable_camera_control', False))).lower())
PY
) )
echo " Existing config found: $CONFIG_DIR/config.json" echo " Existing config found: $CONFIG_DIR/config.json"
fi fi
...@@ -204,6 +218,15 @@ case "$ENABLE_AUDIO_INPUT" in ...@@ -204,6 +218,15 @@ case "$ENABLE_AUDIO_INPUT" in
*) ENABLE_AUDIO_CONTROL=false ;; *) ENABLE_AUDIO_CONTROL=false ;;
esac esac
DEFAULT_CAMERA_CHOICE="n"
[ "$EXISTING_ENABLE_CAMERA_CONTROL" = "true" ] && DEFAULT_CAMERA_CHOICE="y"
read -r -p "Enable camera_control? (y/N) [$DEFAULT_CAMERA_CHOICE]: " ENABLE_CAMERA_INPUT
ENABLE_CAMERA_INPUT=${ENABLE_CAMERA_INPUT:-$DEFAULT_CAMERA_CHOICE}
case "$ENABLE_CAMERA_INPUT" in
y|Y|yes|YES) ENABLE_CAMERA_CONTROL=true ;;
*) ENABLE_CAMERA_CONTROL=false ;;
esac
CAPABILITIES='["exec"]' CAPABILITIES='["exec"]'
if [ "$ENABLE_BROWSER" = true ]; then if [ "$ENABLE_BROWSER" = true ]; then
CAPABILITIES=$(python3 - <<'PY' "$CAPABILITIES" CAPABILITIES=$(python3 - <<'PY' "$CAPABILITIES"
...@@ -245,6 +268,16 @@ print(json.dumps(caps)) ...@@ -245,6 +268,16 @@ print(json.dumps(caps))
PY PY
) )
fi fi
if [ "$ENABLE_CAMERA_CONTROL" = true ]; then
CAPABILITIES=$(python3 - <<'PY' "$CAPABILITIES"
import json, sys
caps = json.loads(sys.argv[1])
if 'camera_control' not in caps:
caps.append('camera_control')
print(json.dumps(caps))
PY
)
fi
cat > "$CONFIG_DIR/config.json" << EOF cat > "$CONFIG_DIR/config.json" << EOF
{ {
...@@ -258,6 +291,7 @@ cat > "$CONFIG_DIR/config.json" << EOF ...@@ -258,6 +291,7 @@ cat > "$CONFIG_DIR/config.json" << EOF
"enable_computer_control": $ENABLE_COMPUTER_CONTROL, "enable_computer_control": $ENABLE_COMPUTER_CONTROL,
"enable_desktop_observe": $ENABLE_DESKTOP_OBSERVE, "enable_desktop_observe": $ENABLE_DESKTOP_OBSERVE,
"enable_audio_control": $ENABLE_AUDIO_CONTROL, "enable_audio_control": $ENABLE_AUDIO_CONTROL,
"enable_camera_control": $ENABLE_CAMERA_CONTROL,
"permissions": { "permissions": {
"deny": ["sudo", "su", "doas", "dd if=", "mkfs", "fdisk", "wipe"], "deny": ["sudo", "su", "doas", "dd if=", "mkfs", "fdisk", "wipe"],
"ask": ["rm -rf", "dd if=", "> /dev/", "chmod", "chown", "mv /", ":/usr/", ":/etc/", ":/bin/", ":/sbin/"], "ask": ["rm -rf", "dd if=", "> /dev/", "chmod", "chown", "mv /", ":/usr/", ":/etc/", ":/bin/", ":/sbin/"],
......
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/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 Agent Installer — Linux Version
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved.
#
# This software is released under the MIT License with a copyleft clause.
# See the LICENSE file for full terms.
#
# INSTALLER DESCRIPTION:
# Self-contained installer for Linux. Embedded: node agent code, init.d script.
# External: pip install websockets (online required).
# No git clone, no network downloads of our files.
set -e
echo "=== Hermes Node Agent Installer (Linux) ==="
echo ""
# Check if running as root (for service setup)
if [ "$EUID" -eq 0 ]; then
RUN_AS_ROOT=true
else
RUN_AS_ROOT=false
echo "⚠️ Not running as root — skipping service installation."
echo " To install as a service, run: sudo $0"
echo ""
fi
# Check for Python 3
if ! command -v python3 &> /dev/null; then
echo "❌ ERROR: Python 3 is required but not found."
echo " Install Python 3 first, then re-run this installer."
exit 1
fi
echo "✓ Python: $(python3 --version)"
# Check for pip (will use it to install websockets)
if ! command -v pip3 &> /dev/null && ! command -v pip &> /dev/null; then
echo "❌ ERROR: pip is required but not found."
echo " Install with: apt install python3-pip (Debian/Ubuntu) or equivalent"
exit 1
fi
PIP_CMD="pip3"
command -v pip3 &> /dev/null || PIP_CMD="pip"
# Install websockets library (only network call)
echo "[1/5] Installing Python dependencies (websockets)..."
# Check if already installed
if python3 -c "import websockets" 2>/dev/null; then
echo "✓ websockets library already installed"
else
if ! $PIP_CMD install --quiet websockets 2>/dev/null; then
echo "⚠️ Could not install via pip (may need network). Trying with user flag..."
if ! $PIP_CMD install --user --quiet websockets 2>/dev/null; then
echo "❌ Failed to install websockets. Try manually: $PIP_CMD install websockets"
exit 1
fi
fi
echo "✓ websockets library installed"
fi
# Determine install locations
if [ "$RUN_AS_ROOT" = true ]; then
AGENT_DIR="/usr/local/bin"
CONFIG_DIR="/etc/hermes-node"
USE_SERVICE=true
else
AGENT_DIR="$HOME/.local/bin"
CONFIG_DIR="$HOME/.config/hermes-node"
USE_SERVICE=false
fi
mkdir -p "$AGENT_DIR"
mkdir -p "$CONFIG_DIR"
# Extract embedded agent files
echo "[2/5] Extracting embedded agent files..."
TMP_EXTRACT=$(mktemp -d)
ORIG_PWD=$(pwd)
cd "$TMP_EXTRACT"
# === EMBEDDED TARBALL (base64) ===
cat <<'TARBALL_DATA' | base64 -d | tar xzf -
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
TARBALL_DATA
# Copy agent script
cp hermes-node-agent/hermes_node_agent.py "$AGENT_DIR/hermes-node-agent"
chmod +x "$AGENT_DIR/hermes-node-agent"
echo "✓ Agent: $AGENT_DIR/hermes-node-agent"
# Copy init.d script (for root installs)
if [ "$USE_SERVICE" = true ]; then
cp hermes-node-agent/hermes-node-agent.init.d /etc/init.d/hermes-node-agent
chmod +x /etc/init.d/hermes-node-agent
echo "✓ Init script: /etc/init.d/hermes-node-agent"
fi
# Copy node_gateway.py (for plugin reference, if needed)
cp hermes-node-agent/node_gateway.py "$AGENT_DIR/hermes-node-gateway.py" 2>/dev/null || true
# Run packaged interactive config installer
if [ -f "$TMP_EXTRACT/hermes-node-agent/install.sh" ]; then
chmod +x "$TMP_EXTRACT/hermes-node-agent/install.sh"
mkdir -p "$CONFIG_DIR"
echo "[4/5] Running interactive configuration installer..."
INSTALLER_TMP_ROOT=$(mktemp -d)
trap 'rm -rf "$INSTALLER_TMP_ROOT" "$TMP_EXTRACT"' EXIT
mkdir -p "$INSTALLER_TMP_ROOT/node-agent"
cp "$TMP_EXTRACT/hermes-node-agent/hermes_node_agent.py" "$INSTALLER_TMP_ROOT/node-agent/hermes_node_agent.py"
cp "$TMP_EXTRACT/hermes-node-agent/browser_controller.py" "$INSTALLER_TMP_ROOT/node-agent/browser_controller.py"
cp "$TMP_EXTRACT/hermes-node-agent/requirements.txt" "$INSTALLER_TMP_ROOT/node-agent/requirements.txt"
cp "$TMP_EXTRACT/hermes-node-agent/install.sh" "$INSTALLER_TMP_ROOT/node-agent/install.sh"
cp "$TMP_EXTRACT/hermes-node-agent/hermes-node-agent.init.d" "$INSTALLER_TMP_ROOT/node-agent/hermes-node-agent.init.d"
cp "$TMP_EXTRACT/hermes-node-agent/hermes-node-agent.service" "$INSTALLER_TMP_ROOT/node-agent/hermes-node-agent.service"
cp "$TMP_EXTRACT/hermes-node-agent/README.md" "$INSTALLER_TMP_ROOT/node-agent/README.md"
cp "$TMP_EXTRACT/hermes-node-agent/LICENSE" "$INSTALLER_TMP_ROOT/node-agent/LICENSE"
cp "$TMP_EXTRACT/hermes-node-agent/DEPLOYMENT.md" "$INSTALLER_TMP_ROOT/node-agent/DEPLOYMENT.md"
cp "$TMP_EXTRACT/hermes-node-agent/PROTOCOL.md" "$INSTALLER_TMP_ROOT/node-agent/PROTOCOL.md"
cp "$TMP_EXTRACT/hermes-node-agent/BROWSER_PROTOCOL.md" "$INSTALLER_TMP_ROOT/node-agent/BROWSER_PROTOCOL.md"
(
cd "$INSTALLER_TMP_ROOT"
bash ./node-agent/install.sh
)
rm -rf "$INSTALLER_TMP_ROOT"
trap 'rm -rf "$TMP_EXTRACT"' EXIT
else
echo "❌ ERROR: install.sh missing from embedded payload"
exit 1
fi
echo "[3/5] Cleaning up temporary files..."
cd "$ORIG_PWD"
rm -rf "$TMP_EXTRACT"
trap - EXIT
# Install SysV init service (root only)
if [ "$USE_SERVICE" = true ]; then
echo ""
echo "[Service] Enabling auto-start on boot..."
update-rc.d hermes-node-agent defaults 2>/dev/null || true
echo "✓ Service: /etc/init.d/hermes-node-agent"
echo ""
echo "Service commands:"
echo " /etc/init.d/hermes-node-agent start|stop|restart|status"
else
echo ""
echo "[Service] Skipped (not root). Manual start required:"
echo " $AGENT_DIR/hermes-node-agent --config $CONFIG_DIR/config.json"
fi
echo ""
echo "=== Installation Complete ==="
echo ""
echo "Quick start:"
echo " Config: $CONFIG_DIR/config.json"
echo " Agent: $AGENT_DIR/hermes-node-agent"
echo " Run: $AGENT_DIR/hermes-node-agent --config $CONFIG_DIR/config.json"
echo ""
if [ "$USE_SERVICE" = true ]; then
echo "To start the service:"
echo " /etc/init.d/hermes-node-agent start"
echo ""
else
echo "To run manually:"
echo " $AGENT_DIR/hermes-node-agent --config $CONFIG_DIR/config.json &"
echo ""
fi
#!/bin/bash #!/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 Agent Installer — Linux (self-contained) # Hermes Node Agent Installer — Linux (self-contained)
set -e set -e
......
#!/bin/bash #!/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.
# Release script: builds Windows installer and publishes to website # Release script: builds Windows installer and publishes to website
# #
# This script: # This script:
......
#!/bin/bash #!/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.
# Upload built Windows installer to website (lisa.nexlab.net) # Upload built Windows installer to website (lisa.nexlab.net)
# #
# Prerequisites: # Prerequisites:
......
#!/bin/bash #!/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 sexec - secure command execution wrapper # Hermes sexec - secure command execution wrapper
# This is a placeholder. Actual sexec implementation goes here. # This is a placeholder. Actual sexec implementation goes here.
if command -v sudo >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then
......
#!/usr/bin/env python3 #!/usr/bin/env python3
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
<!-- 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
<!-- 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. -->
# Windows Installer Build — PyInstaller + NSIS # Windows Installer Build — PyInstaller + NSIS
## Overview ## Overview
......
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
#!/bin/bash #!/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.
# Complete Windows installer build: PyInstaller + NSIS # Complete Windows installer build: PyInstaller + NSIS
# Works on Linux using Wine Windows Python environment # Works on Linux using Wine Windows Python environment
# #
......
#!/bin/bash #!/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.
# Build Windows executables using PyInstaller via Wine # Build Windows executables using PyInstaller via Wine
set -e set -e
cd "$(cd "$(dirname "$0")" && pwd)" cd "$(cd "$(dirname "$0")" && pwd)"
......
# 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 # Hermes Node Protocol
# Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net> # Copyright (c) 2026 Stefy (nextime) Lanza <stefy@nexlab.net>
# All rights reserved. # All rights reserved.
......
# 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.
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
......
# 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.
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
......
#!/usr/bin/env python3
# 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.
#
# This software is released under the MIT License with a copyleft clause.
# See the LICENSE file for full terms.
"""
Hermes Node Agent - Reverse-connection node executor
Connects to Hermes Gateway via WebSocket and executes commands.
Supports optional tools: browser control, computer_control.
Author: Lisa (Hermes AI)
Date: 2026-04-30 (enhanced)
"""
import argparse
import asyncio
import base64
import json
import logging
import os
import shutil
import shlex
import ssl
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional, Dict, Any, List
LOG_PREVIEW_LEN = 120
def _preview_command(command: Any, limit: int = LOG_PREVIEW_LEN) -> str:
"""Return a compact single-line preview for logging."""
if isinstance(command, (list, tuple)):
text = ' '.join(str(part) for part in command)
else:
text = str(command)
text = ' '.join(text.split())
if len(text) > limit:
return text[:limit] + '…'
return text
def _setup_logging(debug: bool = False) -> None:
level = logging.DEBUG if debug else logging.INFO
logging.basicConfig(level=level, format='%(message)s')
def _log_start(node_name: str, tools: list) -> None:
logger.info(f"start ▶ {node_name} — {', '.join(tools)}")
def _log_connect(url: str) -> None:
logger.info(f"connect ▶ {url}")
def _log_tls_disabled() -> None:
logger.info("tls verify disabled")
def _log_connected() -> None:
logger.info("connect ✓")
def _log_disconnected(reason: Any = None) -> None:
if reason:
logger.info(f"disconnect — {reason}")
else:
logger.info("disconnect")
def _log_registering(node_name: str) -> None:
logger.info(f"register ▶ {node_name}")
def _log_registered(node_name: str) -> None:
logger.info(f"register ✓ {node_name}")
def _log_waiting() -> None:
logger.info("waiting for commands")
def _log_exec_received(command: Any) -> None:
logger.info(f"exec ▶ {_preview_command(command)}")
def _log_exec_completed(command: Any, exit_code: Any) -> None:
logger.info(f"exec ✓ exit={exit_code} — {_preview_command(command)}")
def _log_exec_failed(command: Any, error: Any, exit_code: Any = None) -> None:
prefix = f"exec ✗ exit={exit_code}" if exit_code is not None else "exec ✗"
logger.error(f"{prefix} — {_preview_command(command)} — {error}")
def _log_tool_completed(tool_name: str, label: Any, success: bool, error: Any = None) -> None:
mark = '✓' if success else '✗'
suffix = f" — {error}" if error else ''
logger.info(f"{tool_name} {mark} {_preview_command(label)}{suffix}")
def _log_browser_received(command: Any) -> None:
logger.info(f"browser ▶ {_preview_command(command)}")
def _log_cc_received(action: Any) -> None:
logger.info(f"computer ▶ {_preview_command(action)}")
def _log_audio_received(action: Any) -> None:
logger.info(f"audio ▶ {_preview_command(action)}")
def _log_camera_received(action: Any) -> None:
logger.info(f"camera ▶ {_preview_command(action)}")
def _log_reconnect(delay: Any, reason: Any) -> None:
logger.info(f"reconnect in {delay}s — {reason}")
def _log_registration_ack() -> None:
logger.debug("register ack")
def _log_heartbeat_ack() -> None:
logger.debug("heartbeat ack")
def _log_unknown_message(req_type: Any) -> None:
logger.warning(f"unknown message: {req_type}")
def _log_connection_error(message: Any) -> None:
logger.error(f"connection error — {message}")
def _log_config_missing(path: Path) -> None:
logger.error(f"config missing — {path}")
def _log_token_missing() -> None:
logger.error("token missing in config")
def _log_init_warning(component: str, message: Any) -> None:
logger.warning(f"{component} init failed — {message}")
def _log_disabled(component: str, message: str) -> None:
logger.warning(f"{component} disabled — {message}")
def _log_shutdown() -> None:
logger.info("shutdown")
logger = logging.getLogger(__name__)
try:
import websockets
except ImportError:
print("ERROR: websockets library not found. Install with: pip install websockets")
sys.exit(1)
try:
from browser_controller import BrowserController
HAS_BROWSER = True
except ImportError:
HAS_BROWSER = False
logger = logging.getLogger(__name__)
# ═════════════════════════════════════════════════════════════════════════
# DEFAULT CONFIGURATION
# ═════════════════════════════════════════════════════════════════════════
DEFAULT_GATEWAY_TOKEN = 'GATEWAY_TOKEN_MUST_BE_PROVIDED'
DEFAULT_CONFIG = {
'gateway_url': 'wss://localhost:8765',
'node_name': 'unknown',
'token': DEFAULT_GATEWAY_TOKEN,
'reconnect_interval': 5,
'heartbeat_interval': 30,
'gateway_cert_path': None,
'capabilities': ['exec'],
'enable_browser': False,
'enable_computer_control': False,
'enable_desktop_observe': False,
'enable_audio_control': False,
'enable_camera_control': False,
}
# ═════════════════════════════════════════════════════════════════════════
# PLATFORM ABSTRACTION LAYER
# ═════════════════════════════════════════════════════════════════════════
class PlatformError(RuntimeError):
"""Raised when platform-specific operations fail."""
def is_windows() -> bool:
return sys.platform in ('win32', 'cygwin')
def is_linux() -> bool:
return sys.platform.startswith('linux')
def is_macos() -> bool:
return sys.platform == 'darwin'
# ═════════════════════════════════════════════════════════════════════════
# COMMAND EXECUTION ABSTRACTION
# ═════════════════════════════════════════════════════════════════════════
class CommandExecutor:
"""Abstract base class for executing commands with permission enforcement."""
def __init__(self, permission_rules: Dict[str, List[str]]):
self.permissions = permission_rules or {'allow': [], 'deny': [], 'ask': []}
def execute(self, command: Any, approved: bool = False) -> Dict[str, Any]:
"""Execute command respecting permission rules."""
raise NotImplementedError
def _normalize_command_text(self, command: Any) -> str:
if isinstance(command, (list, tuple)):
return ' '.join(str(part) for part in command).strip()
return str(command).strip()
def _check_permission(self, command: Any, approved: bool) -> tuple[bool, str]:
"""Check allow/deny/ask rules.
Returns (allowed, reason). 'ask' means requires approval gate.
Accepts command as string or argv list.
"""
import re
cmd = self._normalize_command_text(command)
# Deny (highest priority)
for pattern in self.permissions.get('deny', []):
if re.search(pattern, cmd, re.IGNORECASE):
return False, f"Denied by pattern '{pattern}'"
# Ask (medium) — only blocks if not approved
if not approved:
for pattern in self.permissions.get('ask', []):
if re.search(pattern, cmd, re.IGNORECASE):
return True, 'ask'
# Allow (explicit)
if self.permissions.get('allow'):
for pattern in self.permissions['allow']:
if re.search(pattern, cmd, re.IGNORECASE):
return True, 'allowed'
return False, "Not in allow list"
return True, 'default-allow'
# ── POSIX ──────────────────────────────────────────────────────────────────
class PosixCommandExecutor(CommandExecutor):
"""POSIX implementation using integrated permission checks and /bin/sh."""
def execute(self, command: Any, approved: bool = False) -> Dict[str, Any]:
allowed, reason = self._check_permission(command, approved)
if not allowed and reason != 'ask':
return {'success': False, 'error': f'Permission denied: {reason}', 'exit_code': 127}
if not approved and reason == 'ask':
return {'success': False, 'error': 'Command requires approval', 'exit_code': 2}
if isinstance(command, (list, tuple)):
cmd = ' '.join(shlex.quote(str(part)) for part in command)
else:
cmd = str(command)
try:
proc = subprocess.Popen(
['/bin/sh', '-c', cmd],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
out, err = proc.communicate(timeout=300)
return {
'success': proc.returncode == 0,
'stdout': out,
'stderr': err,
'exit_code': proc.returncode,
}
except subprocess.TimeoutExpired:
try:
proc.kill()
except Exception:
pass
return {'success': False, 'error': 'Command timed out', 'exit_code': 124}
except Exception as e:
return {'success': False, 'error': str(e), 'exit_code': -1}
# ── WINDOWS ────────────────────────────────────────────────────────────────
class WindowsCommandExecutor(CommandExecutor):
"""Windows implementation using PowerShell with base64-encoded commands.
Design:
- Uses PowerShell 7+ (pwsh.exe) if available, else Windows PowerShell
- Encodes command as base64(utf-16le) to avoid shell quoting issues
- No-Persist policy flags make it a child process
- Returns stdout/stderr as text with exit code
"""
def __init__(self, permission_rules: Dict[str, List[str]]):
super().__init__(permission_rules)
self.powershell = self._find_powershell()
def _find_powershell(self) -> str:
for candidate in [
r'C:\Program Files\PowerShell\7\pwsh.exe',
r'C:\Program Files (x86)\PowerShell\7\pwsh.exe',
r'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe',
]:
if Path(candidate).exists():
return candidate
return 'powershell'
def execute(self, command: str, approved: bool = False) -> Dict[str, Any]:
allowed, reason = self._check_permission(command, approved)
if not allowed and reason != 'ask':
return {'success': False, 'error': f'Permission denied: {reason}', 'exit_code': 127}
if not approved and reason == 'ask':
return {'success': False, 'error': 'Command requires approval', 'exit_code': 2}
try:
import base64
encoded = base64.b64encode(command.encode('utf-16le')).decode('ascii')
proc = subprocess.Popen(
[self.powershell, '-NoProfile', '-NonInteractive',
'-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
creationflags=subprocess.CREATE_NO_WINDOW if hasattr(subprocess, 'CREATE_NO_WINDOW') else 0
)
out, err = proc.communicate(timeout=300)
return {'success': proc.returncode == 0, 'stdout': out, 'stderr': err,
'exit_code': proc.returncode}
except FileNotFoundError:
return {'success': False, 'error': 'PowerShell not found', 'exit_code': 127}
except Exception as e:
return {'success': False, 'error': str(e), 'exit_code': -1}
# ═════════════════════════════════════════════════════════════════════════
# COMPUTER CONTROL LAYER — cross-platform abstraction
# ═════════════════════════════════════════════════════════════════════════
class ComputerControllerBase:
"""Base class for desktop automation, platform-agnostic API."""
def screenshot(self, output_path: Optional[str] = None) -> Dict[str, Any]:
raise NotImplementedError
def mouse_move(self, x: int, y: int) -> Dict[str, Any]:
raise NotImplementedError
def mouse_click(self, button: int = 1) -> Dict[str, Any]:
raise NotImplementedError
def mouse_position(self) -> Dict[str, Any]:
raise NotImplementedError
def type_text(self, text: str) -> Dict[str, Any]:
raise NotImplementedError
def key_press(self, key: str) -> Dict[str, Any]:
raise NotImplementedError
def get_active_window(self) -> Dict[str, Any]:
raise NotImplementedError
# ── POSIX computer control (xdotool + import/ImageMagick) ──────────────────
class PosixComputerController(ComputerControllerBase):
"""Linux X11 automation via command-line tools."""
def __init__(self):
self.display = os.environ.get('DISPLAY', ':0')
def screenshot(self, output_path: Optional[str] = None) -> Dict[str, Any]:
if output_path:
r = self._run(f'import -display {self.display} -window root "{output_path}"')
return {'success': r['success'], 'path': output_path, 'error': r.get('error')}
else:
import base64
try:
result = subprocess.run(
f'import -display {self.display} -window root png:-',
shell=True, capture_output=True, timeout=30
)
if result.returncode == 0:
return {
'success': True, 'format': 'png',
'data': base64.b64encode(result.stdout).decode('ascii'),
'size': len(result.stdout)
}
except Exception as e:
return {'success': False, 'error': str(e)}
return {'success': False, 'error': 'screenshot failed'}
def mouse_move(self, x: int, y: int) -> Dict[str, Any]:
return self._run(f'xdotool mousemove {x} {y}')
def mouse_click(self, button: int = 1) -> Dict[str, Any]:
return self._run(f'xdotool click {button}')
def mouse_position(self) -> Dict[str, Any]:
out = self._run('xdotool getmouselocation --shell')
pos = {}
if out['success']:
for line in out['stdout'].splitlines():
if '=' in line:
k, v = line.split('=', 1)
pos[k] = int(v)
return {'success': out['success'], 'position': pos, 'error': out.get('error')}
def type_text(self, text: str) -> Dict[str, Any]:
# Escape single quotes for shell
safe = text.replace("'", "'\"'\"'")
return self._run(f"xdotool type --delay 1 '{safe}'")
def key_press(self, key: str) -> Dict[str, Any]:
return self._run(f'xdotool key {key}')
def get_active_window(self) -> Dict[str, Any]:
win_id = self._run('xdotool getactivewindow')
if win_id['success']:
title = self._run(f'xdotool getwindowname {win_id["stdout"]}')
return {'success': True, 'window_id': win_id['stdout'],
'title': title.get('stdout', ''), 'error': title.get('error')}
return win_id
def _run(self, cmd: str) -> Dict[str, Any]:
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
return {'success': r.returncode == 0, 'stdout': r.stdout.strip(),
'stderr': r.stderr.strip(), 'exit_code': r.returncode}
except Exception as e:
return {'success': False, 'error': str(e)}
# ── WINDOWS computer control (pyautogui + PIL) ────────────────────────────
class WindowsComputerController(ComputerControllerBase):
"""Windows desktop automation using pyautogui and PIL/Pillow."""
def __init__(self):
try:
import PIL.ImageGrab
import pyautogui
self.ImageGrab = PIL.ImageGrab
self.pyautogui = pyautogui
pyautogui.FAILSAFE = False # Allow user to abort by moving mouse to corner
except ImportError as e:
raise ImportError(f"Windows computer_control requires pyautogui and Pillow: {e}")
def screenshot(self, output_path: Optional[str] = None) -> Dict[str, Any]:
try:
img = self.ImageGrab.grab()
if output_path:
img.save(output_path)
return {'success': True, 'path': output_path}
import io, base64
buf = io.BytesIO()
img.save(buf, format='PNG')
return {
'success': True, 'format': 'png',
'data': base64.b64encode(buf.getvalue()).decode('ascii'),
'size': len(buf.getvalue())
}
except Exception as e:
return {'success': False, 'error': str(e)}
def mouse_move(self, x: int, y: int) -> Dict[str, Any]:
try:
self.pyautogui.moveTo(x, y)
return {'success': True}
except Exception as e:
return {'success': False, 'error': str(e)}
def mouse_click(self, button: int = 1) -> Dict[str, Any]:
try:
btn = {1: 'left', 2: 'middle', 3: 'right'}.get(button, 'left')
self.pyautogui.click(button=btn)
return {'success': True}
except Exception as e:
return {'success': False, 'error': str(e)}
def mouse_position(self) -> Dict[str, Any]:
try:
x, y = self.pyautogui.position()
return {'success': True, 'position': {'x': x, 'y': y}}
except Exception as e:
return {'success': False, 'error': str(e)}
def type_text(self, text: str) -> Dict[str, Any]:
try:
self.pyautogui.write(text, interval=0.01)
return {'success': True}
except Exception as e:
return {'success': False, 'error': str(e)}
def key_press(self, key: str) -> Dict[str, Any]:
try:
key_map = {
'return': 'enter', 'enter': 'enter', 'esc': 'escape',
'ctrl': 'ctrl', 'alt': 'alt', 'shift': 'shift',
'tab': 'tab', 'space': 'space', 'backspace': 'backspace',
'delete': 'delete', 'up': 'up', 'down': 'down',
'left': 'left', 'right': 'right', 'home': 'home', 'end': 'end'
}
mapped = key_map.get(key.lower(), key)
self.pyautogui.press(mapped)
return {'success': True}
except Exception as e:
return {'success': False, 'error': str(e)}
def get_active_window(self) -> Dict[str, Any]:
try:
import win32gui, win32process
hwnd = win32gui.GetForegroundWindow()
title = win32gui.GetWindowText(hwnd)
_, pid = win32process.GetWindowThreadProcessId(hwnd)
return {
'success': True,
'window_id': hwnd,
'title': title,
'process_id': pid
}
except ImportError:
return {'success': True, 'title': self.pyautogui.getActiveWindow().title}
except Exception as e:
return {'success': False, 'error': str(e)}
# ── AUDIO CONTROL ──────────────────────────────────────────────────────────
class AudioControllerBase:
"""Base class for audio device/media actions."""
def capability_info(self) -> Dict[str, Any]:
raise NotImplementedError
def list_audio_devices(self) -> Dict[str, Any]:
raise NotImplementedError
def get_audio_status(self) -> Dict[str, Any]:
raise NotImplementedError
def capture_output(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
def capture_input(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
def play_audio(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
class CameraControllerBase:
"""Base class for camera device/media actions."""
def capability_info(self) -> Dict[str, Any]:
raise NotImplementedError
def list_cameras(self) -> Dict[str, Any]:
raise NotImplementedError
def get_camera_status(self) -> Dict[str, Any]:
raise NotImplementedError
def capture_frame(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
def capture_video(self, params: Dict[str, Any]) -> Dict[str, Any]:
raise NotImplementedError
class PosixAudioController(AudioControllerBase):
"""Linux audio support via ffmpeg + available host backends."""
def __init__(self):
self.ffmpeg = shutil.which('ffmpeg')
if not self.ffmpeg:
raise PlatformError('ffmpeg not found')
self.ffplay = shutil.which('ffplay')
self.ffprobe = shutil.which('ffprobe')
self.pactl = shutil.which('pactl')
self.arecord = shutil.which('arecord')
self.aplay = shutil.which('aplay')
self.backend = self._detect_backend()
def _detect_backend(self) -> str:
if self.pactl:
probe = self._run_quiet([self.pactl, 'info'])
if probe['success']:
server = (probe.get('stdout') or '').lower()
if 'pipewire' in server:
return 'pipewire-pulse'
return 'pulseaudio'
if os.environ.get('PIPEWIRE_RUNTIME_DIR') or os.environ.get('XDG_RUNTIME_DIR'):
return 'pipewire'
if self.arecord:
return 'alsa'
return 'unknown'
def capability_info(self) -> Dict[str, Any]:
monitor_ready, monitor_name = self._default_monitor_source()
input_ready, input_source = self._default_input_source()
return {
'platform': 'linux',
'backend': self.backend,
'available': True,
'can_capture_output': monitor_ready,
'can_capture_input': input_ready,
'can_play_audio': bool(self.ffplay or self.aplay or self.ffmpeg),
'can_inject_mic': False,
'capture_output_ready': monitor_ready,
'capture_output_backend': 'pulseaudio-monitor' if monitor_ready else None,
'default_output_monitor': monitor_name,
'default_input_source': input_source,
'ffmpeg': bool(self.ffmpeg),
'ffplay': bool(self.ffplay),
'pactl': bool(self.pactl),
'arecord': bool(self.arecord),
'aplay': bool(self.aplay),
}
def _run_quiet(self, cmd: List[str], timeout: int = 15) -> Dict[str, Any]:
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
return {
'success': proc.returncode == 0,
'stdout': proc.stdout,
'stderr': proc.stderr,
'exit_code': proc.returncode,
}
except Exception as e:
return {'success': False, 'stdout': '', 'stderr': str(e), 'exit_code': -1}
def _default_output_sink(self) -> Optional[str]:
if not self.pactl:
return None
result = self._run_quiet([self.pactl, 'get-default-sink'])
sink = (result.get('stdout') or '').strip()
if result['success'] and sink:
return sink
return None
def _default_input_source(self) -> tuple[bool, Optional[str]]:
if self.pactl:
result = self._run_quiet([self.pactl, 'get-default-source'])
source = (result.get('stdout') or '').strip()
if result['success'] and source:
return True, source
if self.arecord:
return True, 'default'
return False, None
def _default_monitor_source(self) -> tuple[bool, Optional[str]]:
sink = self._default_output_sink()
if sink:
return True, f'{sink}.monitor'
return False, None
def _expand_output_path(self, path: Optional[str], suffix: str) -> str:
if path:
return str(Path(path).expanduser())
stamp = int(time.time())
return f'/tmp/hermes-audio-{stamp}{suffix}'
def _encode_file(self, path: str) -> Dict[str, Any]:
data = Path(path).read_bytes()
return {
'path': path,
'size_bytes': len(data),
'data_base64': base64.b64encode(data).decode('ascii'),
}
def _media_duration(self, path: str) -> Optional[float]:
if not self.ffprobe:
return None
result = self._run_quiet([
self.ffprobe, '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', path
])
if not result['success']:
return None
try:
return round(float((result.get('stdout') or '').strip()), 3)
except Exception:
return None
def list_audio_devices(self) -> Dict[str, Any]:
devices: Dict[str, Any] = {'backend': self.backend, 'sinks': [], 'sources': []}
if self.pactl:
sink_res = self._run_quiet([self.pactl, 'list', 'short', 'sinks'])
source_res = self._run_quiet([self.pactl, 'list', 'short', 'sources'])
if sink_res['success']:
for line in sink_res.get('stdout', '').splitlines():
parts = line.split('\t')
if len(parts) >= 2:
devices['sinks'].append({'id': parts[0], 'name': parts[1], 'raw': line})
if source_res['success']:
for line in source_res.get('stdout', '').splitlines():
parts = line.split('\t')
if len(parts) >= 2:
devices['sources'].append({'id': parts[0], 'name': parts[1], 'raw': line})
if not devices['sources'] and self.arecord:
src = self._run_quiet([self.arecord, '-l'])
devices['sources_raw'] = src.get('stdout', '')
return {'success': True, **devices}
def get_audio_status(self) -> Dict[str, Any]:
monitor_ready, monitor_name = self._default_monitor_source()
input_ready, input_source = self._default_input_source()
status = {
'success': True,
'backend': self.backend,
'default_output_sink': self._default_output_sink(),
'default_output_monitor': monitor_name,
'default_input_source': input_source,
'capture_output_ready': monitor_ready,
'capture_input_ready': input_ready,
'can_play_audio': bool(self.ffplay or self.aplay or self.ffmpeg),
}
if self.pactl:
info = self._run_quiet([self.pactl, 'info'])
if info['success']:
status['server_info'] = info.get('stdout', '')
return status
def capture_output(self, params: Dict[str, Any]) -> Dict[str, Any]:
duration = max(1, min(int(params.get('duration', 5)), 600))
fmt = str(params.get('format', 'wav')).lower()
path = self._expand_output_path(params.get('output_path') or params.get('path'), f'.{fmt}')
monitor_ready, monitor = self._default_monitor_source()
if not monitor_ready or not monitor:
return {'success': False, 'error': 'No PulseAudio/PipeWire monitor source available for output capture'}
cmd = [
self.ffmpeg, '-y', '-v', 'error', '-f', 'pulse', '-i', monitor,
'-t', str(duration), path,
]
result = self._run_quiet(cmd, timeout=duration + 15)
if not result['success']:
return {'success': False, 'error': (result.get('stderr') or result.get('stdout') or 'ffmpeg capture failed').strip()}
payload = {
'success': True,
'format': fmt,
'duration': duration,
'source': monitor,
}
payload.update(self._encode_file(path))
media_duration = self._media_duration(path)
if media_duration is not None:
payload['measured_duration'] = media_duration
return payload
def capture_input(self, params: Dict[str, Any]) -> Dict[str, Any]:
duration = max(1, min(int(params.get('duration', 5)), 600))
fmt = str(params.get('format', 'wav')).lower()
path = self._expand_output_path(params.get('output_path') or params.get('path'), f'.{fmt}')
source = params.get('source')
input_ready, default_source = self._default_input_source()
if not source:
source = default_source
if self.pactl and source:
cmd = [
self.ffmpeg, '-y', '-v', 'error', '-f', 'pulse', '-i', str(source),
'-t', str(duration), path,
]
result = self._run_quiet(cmd, timeout=duration + 15)
elif self.arecord and input_ready:
cmd = [self.arecord, '-q', '-d', str(duration), path]
result = self._run_quiet(cmd, timeout=duration + 15)
else:
return {'success': False, 'error': 'No usable input capture backend available'}
if not result['success']:
return {'success': False, 'error': (result.get('stderr') or result.get('stdout') or 'input capture failed').strip()}
payload = {
'success': True,
'format': fmt,
'duration': duration,
'source': source,
}
payload.update(self._encode_file(path))
media_duration = self._media_duration(path)
if media_duration is not None:
payload['measured_duration'] = media_duration
return payload
def play_audio(self, params: Dict[str, Any]) -> Dict[str, Any]:
path = params.get('path')
if not path:
return {'success': False, 'error': 'play_audio requires params.path'}
path = str(Path(path).expanduser())
if not Path(path).exists():
return {'success': False, 'error': f'Audio file not found: {path}'}
if self.ffplay:
cmd = [self.ffplay, '-nodisp', '-autoexit', '-loglevel', 'error', path]
elif self.aplay:
cmd = [self.aplay, path]
else:
cmd = [self.ffmpeg, '-v', 'error', '-i', path, '-f', 'null', '-']
result = self._run_quiet(cmd, timeout=max(30, int(params.get('timeout', 120))))
if not result['success']:
return {'success': False, 'error': (result.get('stderr') or result.get('stdout') or 'audio playback failed').strip()}
payload = {'success': True, 'path': path}
media_duration = self._media_duration(path)
if media_duration is not None:
payload['duration'] = media_duration
return payload
# ── Factory functions ─────────────────────────────────────────────────────
def make_executor(config: Dict[str, Any]) -> CommandExecutor:
"""Select appropriate command executor for current platform."""
perms = config.get('permissions', {})
if is_windows():
return WindowsCommandExecutor(perms)
return PosixCommandExecutor(perms)
def make_computer_controller(config: Dict[str, Any]) -> Optional[ComputerControllerBase]:
"""Select and instantiate the appropriate computer controller, or None if deps missing."""
if not config.get('enable_computer_control'):
return None
if is_windows():
try:
return WindowsComputerController()
except ImportError as e:
logger.warning(f"computer_control disabled (missing deps): {e}")
return None
else:
# Linux/macOS
if is_macos():
logger.warning("macOS computer_control not implemented yet")
return None
# Linux
if subprocess.run(['which', 'xdotool'], capture_output=True).returncode != 0:
logger.warning("xdotool not found — computer_control disabled")
return None
try:
return PosixComputerController()
except Exception as e:
logger.warning(f"computer_control init failed: {e}")
return None
def make_audio_controller(config: Dict[str, Any]) -> Optional[AudioControllerBase]:
if not config.get('enable_audio_control'):
return None
if is_linux():
try:
return PosixAudioController()
except Exception as e:
_log_disabled('audio_control', str(e))
return None
_log_disabled('audio_control', f'unsupported platform: {sys.platform}')
return None
class PosixCameraController(CameraControllerBase):
"""Linux camera support via ffmpeg + V4L2 device nodes."""
def __init__(self):
self.ffmpeg = shutil.which('ffmpeg')
if not self.ffmpeg:
raise PlatformError('ffmpeg not found')
self.ffprobe = shutil.which('ffprobe')
self.v4l2_ctl = shutil.which('v4l2-ctl')
def _list_device_paths(self) -> List[Path]:
return sorted(Path('/dev').glob('video*'), key=lambda p: p.name)
def _encode_file(self, path: str) -> Dict[str, Any]:
data = Path(path).read_bytes()
return {
'path': path,
'size_bytes': len(data),
'data_base64': base64.b64encode(data).decode('ascii'),
}
def _media_duration(self, path: str) -> Optional[float]:
if not self.ffprobe:
return None
try:
proc = subprocess.run([
self.ffprobe, '-v', 'error', '-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1', path
], capture_output=True, text=True, timeout=15)
if proc.returncode != 0:
return None
return round(float((proc.stdout or '').strip()), 3)
except Exception:
return None
def _expand_output_path(self, path: Optional[str], suffix: str) -> str:
if path:
return str(Path(path).expanduser())
stamp = int(time.time())
return f'/tmp/hermes-camera-{stamp}{suffix}'
def _ffmpeg_probe(self, device: str) -> Dict[str, Any]:
try:
proc = subprocess.run(
[self.ffmpeg, '-hide_banner', '-f', 'v4l2', '-list_formats', 'all', '-i', device],
capture_output=True,
text=True,
timeout=15,
)
text = '\n'.join(part for part in [proc.stdout, proc.stderr] if part)
return {
'success': proc.returncode in (0, 1),
'output': text.strip(),
'exit_code': proc.returncode,
}
except Exception as e:
return {'success': False, 'output': str(e), 'exit_code': -1}
def _device_info(self, device_path: Path) -> Dict[str, Any]:
info = {
'path': str(device_path),
'name': device_path.name,
'exists': device_path.exists(),
'readable': os.access(device_path, os.R_OK),
'writable': os.access(device_path, os.W_OK),
}
by_id_root = Path('/dev/v4l/by-id')
aliases = []
if by_id_root.exists():
for alias in sorted(by_id_root.iterdir()):
try:
if alias.resolve() == device_path.resolve():
aliases.append(str(alias))
except Exception:
continue
if aliases:
info['aliases'] = aliases
if self.v4l2_ctl:
try:
proc = subprocess.run(
[self.v4l2_ctl, '--device', str(device_path), '--all'],
capture_output=True,
text=True,
timeout=15,
)
info['details'] = (proc.stdout or proc.stderr or '').strip()
info['available'] = proc.returncode == 0
except Exception as e:
info['available'] = False
info['probe_error'] = str(e)
else:
probe = self._ffmpeg_probe(str(device_path))
info['available'] = probe['success']
if probe.get('output'):
info['details'] = probe['output']
return info
def capability_info(self) -> Dict[str, Any]:
devices = self._list_device_paths()
return {
'platform': 'linux',
'backend': 'v4l2-ffmpeg',
'available': bool(devices),
'device_count': len(devices),
'supports_frame_capture': True,
'supports_video_capture': True,
'ffmpeg': bool(self.ffmpeg),
'ffprobe': bool(self.ffprobe),
'v4l2_ctl': bool(self.v4l2_ctl),
'devices': [str(p) for p in devices],
}
def list_cameras(self) -> Dict[str, Any]:
devices = [self._device_info(path) for path in self._list_device_paths()]
return {
'success': True,
'backend': 'v4l2-ffmpeg',
'camera_count': len(devices),
'cameras': devices,
}
def get_camera_status(self) -> Dict[str, Any]:
devices = self.list_cameras()
payload = {
'success': True,
'backend': 'v4l2-ffmpeg',
'ffmpeg': bool(self.ffmpeg),
'ffprobe': bool(self.ffprobe),
'v4l2_ctl': bool(self.v4l2_ctl),
'camera_count': devices.get('camera_count', 0),
'cameras': devices.get('cameras', []),
}
if payload['camera_count'] == 0:
payload['available'] = False
payload['reason'] = 'No /dev/video* devices found'
else:
payload['available'] = True
return payload
def _pick_device(self, params: Dict[str, Any]) -> str:
device = params.get('device') or params.get('device_path')
if device:
return str(Path(str(device)).expanduser())
devices = self._list_device_paths()
if not devices:
raise PlatformError('No /dev/video* devices found')
return str(devices[0])
def capture_frame(self, params: Dict[str, Any]) -> Dict[str, Any]:
device = self._pick_device(params)
fmt = str(params.get('format', 'png')).lower()
if fmt not in ('png', 'jpg', 'jpeg', 'bmp'):
return {'success': False, 'error': f'Unsupported frame format: {fmt}'}
suffix = '.jpg' if fmt == 'jpeg' else f'.{fmt}'
path = self._expand_output_path(params.get('output_path') or params.get('path'), suffix)
width = params.get('width')
height = params.get('height')
cmd = [self.ffmpeg, '-y', '-v', 'error', '-f', 'v4l2']
if width and height:
cmd += ['-video_size', f'{int(width)}x{int(height)}']
cmd += ['-i', device, '-frames:v', '1', path]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
except Exception as e:
return {'success': False, 'error': str(e)}
if proc.returncode != 0:
return {'success': False, 'error': (proc.stderr or proc.stdout or 'frame capture failed').strip()}
payload = {
'success': True,
'device': device,
'format': fmt,
}
payload.update(self._encode_file(path))
return payload
def capture_video(self, params: Dict[str, Any]) -> Dict[str, Any]:
device = self._pick_device(params)
duration = max(1, min(int(params.get('duration', 5)), 600))
fmt = str(params.get('format', 'mp4')).lower()
extension = 'mkv' if fmt == 'matroska' else fmt
if extension not in ('mp4', 'mkv', 'webm'):
return {'success': False, 'error': f'Unsupported video format: {fmt}'}
path = self._expand_output_path(params.get('output_path') or params.get('path'), f'.{extension}')
width = params.get('width')
height = params.get('height')
fps = params.get('fps')
cmd = [self.ffmpeg, '-y', '-v', 'error', '-f', 'v4l2']
if fps:
cmd += ['-framerate', str(fps)]
if width and height:
cmd += ['-video_size', f'{int(width)}x{int(height)}']
cmd += ['-i', device, '-t', str(duration), path]
try:
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=duration + 30)
except Exception as e:
return {'success': False, 'error': str(e)}
if proc.returncode != 0:
return {'success': False, 'error': (proc.stderr or proc.stdout or 'video capture failed').strip()}
payload = {
'success': True,
'device': device,
'format': extension,
'duration': duration,
}
payload.update(self._encode_file(path))
media_duration = self._media_duration(path)
if media_duration is not None:
payload['measured_duration'] = media_duration
return payload
def make_camera_controller(config: Dict[str, Any]) -> Optional[CameraControllerBase]:
if not config.get('enable_camera_control'):
return None
if is_linux():
try:
controller = PosixCameraController()
if not controller._list_device_paths():
_log_disabled('camera_control', 'no /dev/video* devices found')
return None
return controller
except Exception as e:
_log_disabled('camera_control', str(e))
return None
_log_disabled('camera_control', f'unsupported platform: {sys.platform}')
return None
class NodeAgent:
def __init__(self, config_path: Optional[str] = None):
self.config = self._load_config(config_path)
self.executor = make_executor(self.config)
self.computer = make_computer_controller(self.config)
self.audio = make_audio_controller(self.config)
self.camera = make_camera_controller(self.config)
self.browser = None
if self.config.get('enable_browser') and HAS_BROWSER:
try:
self.browser = BrowserController()
except Exception as e:
logger.warning(f"BrowserController init failed: {e}")
self.capabilities = self._detect_capabilities()
def _load_config(self, path: Optional[str]) -> Dict[str, Any]:
"""Load node configuration from JSON."""
cfg_path = Path(path).expanduser() if path else Path.home() / '.config' / 'hermes-node' / 'config.json'
if not cfg_path.exists():
logger.error(f"Config not found: {cfg_path}")
logger.error(f"Run the installer or copy config-template.json to {cfg_path}")
sys.exit(1)
try:
with open(cfg_path) as f:
data = json.load(f)
except json.JSONDecodeError as e:
logger.error(f"Config file is not valid JSON: {e}")
logger.error(f"File: {cfg_path}")
sys.exit(1)
# Merge defaults
merged = {**DEFAULT_CONFIG, **data}
if not merged['token']:
logger.error("Token missing from config")
sys.exit(1)
return merged
def _detect_capabilities(self) -> Dict[str, Any]:
"""Detect which optional tools are available on this machine."""
caps = {
'enable_browser': self.config.get('enable_browser', False),
'enable_computer_control': self.config.get('enable_computer_control', False),
'enable_desktop_observe': self.config.get('enable_desktop_observe', False),
'enable_audio_control': self.config.get('enable_audio_control', False),
'enable_camera_control': self.config.get('enable_camera_control', False),
}
if self.browser is not None:
caps['browser_control'] = {'available': True}
if self.computer is not None:
cc_info = {
'display': os.environ.get('DISPLAY', ':0'),
'has_xdotool': subprocess.run(['which', 'xdotool'], capture_output=True).returncode == 0,
'has_import': subprocess.run(['which', 'import'], capture_output=True).returncode == 0,
'has_scrot': subprocess.run(['which', 'scrot'], capture_output=True).returncode == 0,
}
caps['computer_control'] = cc_info
if caps['enable_desktop_observe']:
caps['desktop_observe'] = {
'available': self.computer is not None,
'display': os.environ.get('DISPLAY', ':0')
}
if caps['enable_audio_control']:
if self.audio is not None:
caps['audio_control'] = self.audio.capability_info()
else:
caps['audio_control'] = {
'available': False,
'reason': 'audio control requested but backend dependencies are unavailable'
}
if caps['enable_camera_control']:
if self.camera is not None:
caps['camera_control'] = self.camera.capability_info()
else:
caps['camera_control'] = {
'available': False,
'reason': 'camera control requested but no supported camera backend/devices are available'
}
return caps
async def connect_and_run(self):
"""Main loop: connect to gateway and process commands."""
url = f"{self.config['gateway_url']}?node_name={self.config['node_name']}&token={self.config['token']}"
_log_connect(url)
ssl_context = None
if url.startswith('wss://'):
cert_path = self.config.get('gateway_cert_path')
if cert_path:
ssl_context = ssl.create_default_context(cafile=str(Path(cert_path).expanduser()))
else:
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
_log_tls_disabled()
while True:
try:
async with websockets.connect(url, ping_interval=20, ping_timeout=10, ssl=ssl_context) as ws:
_log_connected()
# Send registration frame expected by the gateway
_log_registering(self.config['node_name'])
await ws.send(json.dumps({
"type": "register",
"node_name": self.config['node_name'],
"version": "1.0",
"tools": self._get_available_tools(),
"capabilities": self.capabilities
}))
_log_waiting()
heartbeat_task = asyncio.create_task(self._heartbeat_loop(ws))
disconnect_reason = None
try:
async for raw in ws:
msg = json.loads(raw)
await self._handle_message(ws, msg)
except Exception as e:
disconnect_reason = e
raise
finally:
heartbeat_task.cancel()
try:
await heartbeat_task
except asyncio.CancelledError:
pass
_log_disconnected(disconnect_reason)
except Exception as e:
_log_connection_error(e)
_log_reconnect(self.config['reconnect_interval'], e)
await asyncio.sleep(self.config['reconnect_interval'])
def _get_available_tools(self) -> list:
"""Return list of capability strings for gateway registration."""
tools = ['exec']
if self.browser is not None:
tools.append('browser_control')
if self.computer is not None:
tools.append('computer_control')
if self.config.get('enable_desktop_observe') and self.computer is not None:
tools.append('desktop_observe')
if self.config.get('enable_audio_control') and self.audio is not None:
tools.append('audio_control')
if self.config.get('enable_camera_control') and self.camera is not None:
tools.append('camera_control')
return tools
async def _handle_message(self, ws, msg: Dict[str, Any]):
req_type = msg.get('type')
if req_type == 'exec':
cmd_id = msg['id']
command = msg['command']
await self._handle_exec(ws, cmd_id, command, msg.get('approved', False))
elif req_type == 'computer_control':
action = msg['action']
params = msg.get('params', {})
await self._handle_cc(ws, msg.get('id'), action, params)
elif req_type == 'desktop_observe':
action = msg['action']
params = msg.get('params', {})
await self._handle_desktop_observe(ws, msg.get('id'), action, params)
elif req_type == 'browser_control':
command = msg.get('command')
params = msg.get('params', {})
await self._handle_browser_control(ws, msg.get('id'), command, params)
elif req_type == 'audio_control':
action = msg['action']
params = msg.get('params', {})
await self._handle_audio_control(ws, msg.get('id'), action, params)
elif req_type == 'camera_control':
action = msg['action']
params = msg.get('params', {})
await self._handle_camera_control(ws, msg.get('id'), action, params)
elif req_type == 'register_ack':
_log_registration_ack()
_log_registered(self.config['node_name'])
elif req_type == 'heartbeat_ack':
_log_heartbeat_ack()
return
else:
_log_unknown_message(req_type)
async def _heartbeat_loop(self, ws):
"""Send periodic heartbeats so the gateway can track liveness."""
interval = max(5, int(self.config.get('heartbeat_interval', 30)))
try:
while True:
await asyncio.sleep(interval)
await ws.send(json.dumps({
'type': 'heartbeat',
'node_name': self.config['node_name'],
'timestamp': time.time(),
}))
except asyncio.CancelledError:
raise
except Exception as e:
_log_connection_error(f"heartbeat loop stopped: {e}")
async def _send_json(self, ws, payload: Dict[str, Any]):
await ws.send(json.dumps(payload))
async def _handle_exec(self, ws, cmd_id: str, command: str, approved: bool = False):
"""Execute a shell command via the configured executor.
Uses the gateway's current exec protocol:
- optional streamed chunks via ``exec_output``
- terminal result via ``exec_complete``
"""
_log_exec_received(command)
try:
result = self.executor.execute(command, approved=approved)
stdout = result.get('stdout', '') or ''
stderr = result.get('stderr', '') or ''
if stdout:
await self._send_json(ws, {
'type': 'exec_output',
'id': cmd_id,
'stream': 'stdout',
'data': stdout,
})
if stderr:
await self._send_json(ws, {
'type': 'exec_output',
'id': cmd_id,
'stream': 'stderr',
'data': stderr,
})
exit_code = result.get('exit_code', -1)
error = result.get('error')
if error:
_log_exec_failed(command, error, exit_code)
else:
_log_exec_completed(command, exit_code)
await self._send_json(ws, {
'type': 'exec_complete',
'id': cmd_id,
'exit_code': exit_code,
'error': error,
})
except Exception as e:
_log_exec_failed(command, str(e), -1)
await self._send_json(ws, {
'type': 'exec_complete',
'id': cmd_id,
'exit_code': -1,
'error': str(e),
})
async def _handle_cc(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
_log_cc_received(action)
if self.computer is None:
result = {
'success': False,
'error': 'computer_control not available on this node',
}
else:
try:
if action == 'screenshot':
result = self.computer.screenshot(params.get('output_path'))
elif action == 'mouse_move':
result = self.computer.mouse_move(int(params['x']), int(params['y']))
elif action == 'mouse_click':
result = self.computer.mouse_click(int(params.get('button', 1)))
elif action == 'mouse_position':
result = self.computer.mouse_position()
elif action == 'type_text':
result = self.computer.type_text(params['text'])
elif action == 'key_press':
result = self.computer.key_press(params['key'])
elif action == 'get_active_window':
result = self.computer.get_active_window()
else:
result = {'success': False, 'error': f'Unknown computer_control action: {action}'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('computer', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'computer_control_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload)
async def _handle_desktop_observe(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
logger.info(f"observe ▶ {_preview_command(action)}")
if self.computer is None:
result = {
'success': False,
'error': 'desktop_observe requires computer_control support on this node',
}
else:
try:
if action in ('active_window', 'get_active_window'):
result = self.computer.get_active_window()
elif action == 'mouse_position':
result = self.computer.mouse_position()
elif action == 'screenshot':
result = self.computer.screenshot(params.get('output_path'))
else:
result = {'success': False, 'error': f'Unknown desktop_observe action: {action}'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('observe', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'desktop_observe_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload)
async def _handle_browser_control(self, ws, cmd_id: str, command: str, params: Dict[str, Any]):
if self.browser is None:
await self._send_json(ws, {
'type': 'browser_control_result',
'id': cmd_id,
'command': command,
'success': False,
'error': 'browser_control not available on this node',
})
return
_log_browser_received(command)
try:
if hasattr(self.browser, 'execute'):
result = self.browser.execute(command, params)
elif hasattr(self.browser, 'run'):
result = self.browser.run(command, params)
else:
result = {'success': False, 'error': 'BrowserController has no execute/run entrypoint'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('browser', command, bool(result.get('success')), result.get('error'))
payload = {'type': 'browser_control_result', 'id': cmd_id, 'command': command}
payload.update(result)
await self._send_json(ws, payload)
async def _handle_audio_control(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
_log_audio_received(action)
if self.audio is None:
await self._send_json(ws, {
'type': 'audio_control_result',
'id': cmd_id,
'action': action,
'success': False,
'error': 'audio_control not available on this node',
})
return
try:
if action == 'list_audio_devices':
result = self.audio.list_audio_devices()
elif action == 'get_audio_status':
result = self.audio.get_audio_status()
elif action == 'capture_output':
result = self.audio.capture_output(params)
elif action == 'capture_input':
result = self.audio.capture_input(params)
elif action == 'play_audio':
result = self.audio.play_audio(params)
else:
result = {'success': False, 'error': f'Unknown audio_control action: {action}'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('audio', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'audio_control_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload)
async def _handle_camera_control(self, ws, cmd_id: str, action: str, params: Dict[str, Any]):
_log_camera_received(action)
if self.camera is None:
await self._send_json(ws, {
'type': 'camera_control_result',
'id': cmd_id,
'action': action,
'success': False,
'error': 'camera_control not available on this node',
})
return
try:
if action == 'list_cameras':
result = self.camera.list_cameras()
elif action == 'get_camera_status':
result = self.camera.get_camera_status()
elif action == 'capture_frame':
result = self.camera.capture_frame(params)
elif action == 'capture_video':
result = self.camera.capture_video(params)
else:
result = {'success': False, 'error': f'Unknown camera_control action: {action}'}
except Exception as e:
result = {'success': False, 'error': str(e)}
_log_tool_completed('camera', action, bool(result.get('success')), result.get('error'))
payload = {'type': 'camera_control_result', 'id': cmd_id, 'action': action}
payload.update(result)
await self._send_json(ws, payload)
def main():
parser = argparse.ArgumentParser(description="Hermes Node Agent")
parser.add_argument('--config', type=str, help='Path to config JSON')
parser.add_argument('--debug', action='store_true', help='Debug logging')
args = parser.parse_args()
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
# Load config to check token
config = NodeAgent(args.config)._load_config(args.config)
if config['token'] == DEFAULT_GATEWAY_TOKEN or config['token'] == 'GATEWAY_TOKEN_MUST_BE_PROVIDED':
logger.error("ERROR: Token not set in config. Edit ~/.config/hermes-node/config.json")
sys.exit(1)
agent = NodeAgent(args.config)
_log_start(config['node_name'], agent._get_available_tools())
try:
asyncio.run(agent.connect_and_run())
except KeyboardInterrupt:
logger.info("Shutting down")
if __name__ == '__main__':
main()
; 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 Agent Windows Installer — NSIS ; Hermes Node Agent Windows Installer — NSIS
; User-owned config: $APPDATA\HermesNode\config.json ; User-owned config: $APPDATA\HermesNode\config.json
......
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