Commit 8edcac71 authored by Lisa (Hermes AI)'s avatar Lisa (Hermes AI)

docs: add LICENSE, update README with security and repo links

parents
Pipeline #309 canceled with stages
# Hermes Node Gateway Plugin - Changelog
## Version 1.0.0 (2026-04-30)
### Initial Release
**Core Features:**
- ✅ Embedded WebSocket server (runs in Hermes process, port 8765)
- ✅ Token-based node authentication
- ✅ Reverse-connection architecture (nodes connect to gateway)
- ✅ TLS/SSL support (WSS)
- ✅ Sexec permission system integration (allow/ask/deny lists)
- ✅ Real-time command output streaming
- ✅ Heartbeat monitoring
- ✅ Auto-reconnect support (node-side)
**Tools Provided:**
1. **node_list** - List all connected nodes
- Returns: node name, status, uptime, version, capabilities
2. **node_status** - Get detailed status of a specific node
- Returns: connection info, last seen, sexec path
3. **node_exec** - Execute shell commands on remote nodes
- Supports: command arrays, timeout, approval bypass
- Returns: stdout, stderr, exit code, duration
4. **browser_control** - Control browsers on remote nodes via Playwright
- Layers: high_level (simple actions), playwright (full API), cdp (Chrome DevTools)
- Commands: launch, navigate, click, fill, screenshot, execute_script, etc.
- Returns: command-specific results (screenshots, page content, etc.)
**Architecture:**
- Plugin runs as background thread within Hermes Agent process
- No separate gateway daemon needed
- All node communication via authenticated WebSocket
- Thread-safe command execution with async/await
- Command waiters for synchronous tool calls
**Configuration:**
- Configured via `~/.hermes/config.yaml`
- Tokens per node (sissy, zeiss, spank, ganeti1, ganeti2)
- Customizable bind address, port, TLS settings
- Permission system path configurable
**Security:**
- No SSH keys stored on gateway
- Pre-shared token authentication per node
- TLS encryption for all traffic
- Sexec permission enforcement (preserves existing allow/deny/ask lists)
- Approval workflow for dangerous commands
**Deployment:**
- Plugin: `~/.hermes/plugins/hermes-node-gateway/`
- Node agents: deployed separately on each remote machine
- Nodes connect to gateway on startup
- Gateway starts automatically when Hermes loads
**Known Limitations:**
- Requires `websockets` library (pip install websockets)
- Browser control requires Playwright on nodes
- TLS certificates must be generated manually
- No HTTP API (all access via Hermes tools)
**Future Enhancements:**
- [ ] Multi-user approval workflow
- [ ] Command history and audit log
- [ ] Node health metrics
- [ ] Browser session persistence
- [ ] File transfer support
- [ ] Interactive shell sessions
This diff is collapsed.
MIT License
Copyright (c) 2026 Lisa (Hermes AI) / OpenClaw Project
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Hermes Node Gateway Plugin
Integrated node management plugin for Hermes Agent. Runs an embedded WebSocket server within the Hermes process to accept reverse connections from remote nodes.
## Features
- 🔐 **No SSH keys** — Nodes initiate WSS connections to gateway
- 🔥 **Firewall-friendly** — Outbound connections from nodes
-**Preserves sexec permissions** — Reuses existing allow/deny/ask lists
- 🔑 **Token authentication** — Unique pre-shared token per node
- 📡 **Real-time streaming** — Command output streams over WebSocket
- 🔄 **Auto-reconnect** — Nodes reconnect automatically on disconnect
- 💓 **Heartbeat monitoring** — Detect dead connections
- 🧵 **Embedded server** — Runs in-process, no separate gateway daemon
## Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Hermes Agent (main process) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Node Gateway Plugin (background thread) │ │
│ │ - WebSocket server (port 8765) │ │
│ │ - Token authentication │ │
│ │ - Node registry │ │
│ └─────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌─────────────────▼───────────────────────────────┐ │
│ │ Tools: node_list, node_status, node_exec │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ WSS (node-initiated)
┌────────┴─────────────────────────────────────────────┐
│ Remote Node Agent │
│ - Connects to wss://gateway:8765?token=xxx │
│ - Executes commands via sexec │
│ - Streams stdout/stderr back │
└──────────────────────────────────────────────────────┘
```
## Installation
### Installation
#### 1. Enable the Plugin
Add to `~/.hermes/config.yaml`:
```yaml
plugins:
enabled:
- hermes-node-gateway
# Node gateway configuration
node_websocket_port: 8765
node_bind_address: "0.0.0.0"
node_use_tls: true
node_cert_dir: "/home/lisa/hermes-node-protocol/gateway/certs"
node_permissions_path: "/home/lisa/hermes-node-protocol/node-agent/permissions.json"
# Node tokens (one per node) — keep these secret!
# Generate with: openssl rand -hex 64
node_tokens:
sissy: "<token>"
zeiss: "<token>"
spank: "<token>"
ganeti1: "<token>"
ganeti2: "<token>"
```
### 2. Install Dependencies
```bash
pip install websockets
```
### 3. Generate TLS Certificates (if using TLS)
```bash
cd ~/hermes-node-protocol/gateway/certs
openssl req -x509 -newkey rsa:4096 -keyout gateway.key -out gateway.crt -days 365 -nodes -subj "/CN=lisa"
```
### 4. Deploy Node Agents
On each remote node, install the node agent:
```bash
# Copy installer to node
scp -r ~/hermes-node-protocol/node-agent user@node:/tmp/
# SSH to node and install
ssh user@node
cd /tmp/node-agent
./install.sh
# Edit config with gateway URL and token
nano /etc/hermes-node/config.json
# Start agent
/etc/init.d/hermes-node-agent start
```
## Usage
### List Connected Nodes
```python
# Via tool
result = node_list()
# Returns: {'nodes': [{'name': 'sissy', 'status': 'connected', ...}, ...]}
```
### Check Node Status
```python
result = node_status(node_name='sissy')
# Returns: {'name': 'sissy', 'status': 'connected', 'uptime': 3600, ...}
```
### Execute Command on Node
```python
result = node_exec(
node_name='sissy',
command=['df', '-h'],
timeout=30,
approved=False
)
# Returns: {
# 'id': 'cmd-abc123',
# 'status': 'completed',
# 'stdout': 'Filesystem Size Used Avail Use%...',
# 'stderr': '',
# 'exit_code': 0,
# 'duration_ms': 234
# }
```
### Natural Language Examples
Just ask Hermes:
- "Check disk usage on sissy"
- "List Ganeti instances on ganeti1"
- "Check uptime on all nodes"
Hermes will use the `node_exec` tool automatically.
## Permission System
Commands are filtered through the sexec permission system on each node:
### Allow List (auto-execute)
```json
{
"allow": [
"df -h",
"hostname",
"uptime",
"sudo gnt-instance list"
]
}
```
### Ask List (requires approval)
```json
{
"ask": [
"sudo gnt-instance stop *",
"rm -rf *"
]
}
```
### Deny List (rejected immediately)
```json
{
"deny": [
"chmod 777 /",
"> /dev/sda"
]
}
```
## Troubleshooting
### Plugin not loading
Check Hermes logs:
```bash
hermes logs --follow | grep -i node
```
### Node won't connect
1. Check gateway is running (plugin loaded)
2. Verify firewall allows port 8765
3. Check token matches in both configs
4. Check node logs: `tail -f /var/log/hermes-node-agent.log`
### Command denied
The command is in the deny list. Edit `permissions.json` on the node.
### Command requires approval
The command is in the ask list. Re-run with `approved=True` after user confirmation.
## Browser Control
The node gateway also supports remote browser control via Playwright on nodes that have
browser capabilities. This uses the same WebSocket connection and authentication system.
### Available Browser Commands
**High-Level Commands:**
- `launch` - Launch a browser instance (headless/headed/attach)
- `create_context` - Create a browser context
- `new_page` - Create a new page in a context
- `navigate` - Navigate to a URL
- `click` - Click an element
- `fill` - Fill an input field
- `type_text` - Type text with keystrokes
- `wait_for_selector` - Wait for an element
- `screenshot` - Take a screenshot
- `execute_script` - Execute JavaScript
- `evaluate` - Evaluate JavaScript expression
- `get_content` - Get page HTML content
- `get_title` - Get page title
- `list_pages` - List all pages
- `list_contexts` - List all contexts
- `close_page` - Close a page
- `close_context` - Close a context
- `close` - Close the browser
**Example:**
```python
# Navigate to a page and take screenshot
result = browser_control(
node_name="sissy",
command="navigate",
layer="high_level",
params={"url": "https://example.com", "wait_until": "load"}
)
result = browser_control(
node_name="sissy",
command="screenshot",
layer="high_level",
params={"full_page": True}
)
```
## Security Notes
1. **No SSH keys on gateway** — Gateway never stores SSH keys
2. **Nodes connect out** — Firewall-friendly
3. **Token authentication** — Each node has unique pre-shared token
4. **TLS encryption** — All traffic encrypted via WSS
5. **Permission system** — Reuses existing sexec allow/deny/ask lists
## Development
The plugin runs as a background thread within the Hermes Agent process. It provides an embedded WebSocket server for reverse-connected node execution.
**Key files:**
- `plugin.yaml` — Plugin manifest
- `__init__.py` — Plugin implementation (WebSocket server + tools)
- `README.md` — This file
**Related Repositories**
- **Node Agent:** `git@git.nexlab.net:lisa/hermes-node-agent.git`
- **Chrome Extension:** `git@git.nexlab.net:lisa/hermes-node-chrome.git`
**Testing:**
```bash
# Start Hermes with the plugin
hermes
# In another terminal, test node connection
cd ~/hermes-node-protocol/node-agent
python3 hermes_node_agent.py
# Check logs
hermes logs --follow | grep -i node
```
## License
MIT License — See the [LICENSE](LICENSE) file in this repository.
Created by Lisa (Hermes AI) for Stefy
Date: 2026-04-30
# Hermes Node Gateway — User-Space Configuration
**Everything runs as a normal user. No root required.**
---
## Gateway (Plugin)
The gateway plugin runs inside the Hermes Agent process as user `lisa`.
### Configuration Location
All gateway config is in `~/.config/hermes-node-gateway/`:
```
~/.config/hermes-node-gateway/
├── certs/
│ ├── gateway.crt # TLS certificate (self-signed)
│ └── gateway.key # TLS private key
├── config.json # Node tokens (admin-managed)
└── permissions.json # Command permissions (allow/deny/ask)
```
### config.json Format
```json
{
"tokens": {
"sissy": "dbed0834bfc502f3017add9be902c9d321c9cd62f09732a55ee2f8b2b633622f",
"zeiss": "6e07f6490f9e651c8bfdea10f66f0473fa091161d97d32650bb938d9070283e7",
"spank": "ee0e5c368bb0ed144a6952e0b9171aac00a07e161c59c90d08fc9515c8b6f610",
"ganeti1": "565d71e7aba1379940756f6b20c6c515a39b276d042ef1c87f1adcb405a42955",
"ganeti2": "d05d909b0e0c890a075cee34d3e2036013e716dbbb757017a40d8b4c58d98436"
},
"permissions_path": "~/.config/hermes-node-gateway/permissions.json"
}
```
### permissions.json Format
```json
{
"allow": [],
"deny": [],
"ask": [".*"]
}
```
### Gateway Startup
The plugin auto-starts when Hermes loads. It:
- Loads tokens from `~/.config/hermes-node-gateway/config.json`
- Loads TLS certs from `~/.config/hermes-node-gateway/certs/`
- Starts WebSocket server on port 8765 (WSS if certs present)
- Registers tools: `node_list`, `node_status`, `node_exec`, `browser_control`
---
## Node Agent (Installer)
The universal installer (`dist/install_hermes_node.sh`) is **fully user-space**.
### Installation Behavior
- **NO SUDO REQUIRED** to run the installer
- Agent installed to: `~/.local/bin/hermes-node-agent`
- Config written to: `~/.config/hermes-node/config.json`
- Sudo only needed if user chooses to install the optional SysV init service
### Node config.json Format
```json
{
"gateway_url": "wss://lisa:8765",
"node_name": "sissy",
"token": "dbed0834bfc502f3017add9be902c9d321c9cd62f09732a55ee2f8b2b633622f",
"sexec_path": "/home/user/.openclaw/skills/sexec/sexec.sh",
"reconnect_interval": 5,
"heartbeat_interval": 30
}
```
### Running the Agent
```bash
# Manual start (foreground)
~/.local/bin/hermes-node-agent
# Background
~/.local/bin/hermes-node-agent &
# With init service (if installed)
sudo /etc/init.d/hermes-node-agent start
```
---
## Distribution
### Gateway Setup (one-time)
1. Generate TLS certs (already done):
```bash
mkdir -p ~/.config/hermes-node-gateway/certs
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout ~/.config/hermes-node-gateway/certs/gateway.key \
-out ~/.config/hermes-node-gateway/certs/gateway.crt \
-subj '/CN=lisa/O=HermesNodeGateway/C=ZA'
```
2. Create token config:
```bash
cat > ~/.config/hermes-node-gateway/config.json <<EOF
{
"tokens": {
"node1": "token1-here",
"node2": "token2-here"
}
}
EOF
chmod 600 ~/.config/hermes-node-gateway/config.json
```
3. Restart Hermes to load the plugin
### Node Setup (per machine)
1. Copy installer to target machine:
```bash
scp ~/.hermes/plugins/hermes-node-gateway/dist/install_hermes_node.sh user@target:
```
2. Run as normal user:
```bash
bash install_hermes_node.sh
```
3. When prompted, provide:
- Node name (e.g., `sissy`)
- Gateway host (e.g., `lisa`)
- Gateway port (e.g., `8765`)
- Token (from gateway admin)
- sexec.sh path
4. Agent connects outbound to gateway via WSS
---
## Security
- **TLS/WSS**: All traffic encrypted (self-signed cert)
- **Token auth**: Each node has unique token
- **Outbound only**: Nodes connect to gateway (no inbound firewall rules)
- **Permission system**: Commands filtered via allow/deny/ask lists
- **User-space**: No root privileges required for normal operation
- **Secrets**: Tokens stored in user config (600 perms), never in installer
---
## Tools Available
Once nodes are connected, use from any Hermes session:
```bash
# List connected nodes
hermes node_list
# Get node status
hermes node_status --node sissy
# Execute command
hermes node_exec --node sissy --command "uptime"
# Browser control (if browser extension installed)
hermes browser_control --node sissy --action navigate --url "https://example.com"
```
---
## Troubleshooting
### Gateway not starting
- Check: `~/.config/hermes-node-gateway/config.json` exists
- Check: TLS certs at `~/.config/hermes-node-gateway/certs/`
- Check Hermes logs for plugin errors
### Node can't connect
- Verify gateway is running: `telnet lisa 8765`
- Check token matches gateway config
- Check firewall allows outbound to port 8765
- Check node logs: `~/.local/bin/hermes-node-agent` (foreground)
### Permission denied
- Check `~/.config/hermes-node-gateway/permissions.json`
- Default is `ask` for all commands
- Add patterns to `allow` list for auto-approval
---
**Everything is user-space. No /etc, no /var, no root.**
This diff is collapsed.
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC7JwBvSw9oLq5o
lGjlk2i/3IA0eyTjCpkRzzId4WMTM8ohV6bxEMEu+gj44wpeoPMNelDkw35hH1cE
bT4dV7vlKcrFfRwxQNEPWwQ8Rm/Uz8oQ3iM+Oad+CIzZuBkK5GOcyjJxGDFHTasS
5ztZcfmK0nwYx4R/WxXssqUeU2hinhUjNy7YQ0yK4UPLn42MkVsEwPQjXY8e8I+3
GKda8HXEqditX+7LRw62eNzmVvhS4YMcfnBxhNwAB8ai4r9++V0Acz4uSgjmaq8j
ANJ0vTWyxtCFzoox7kiE+STYNRvuXemw/P4oAlu8d1jzSOLqaFkBazmzbfMUuypw
FDevBwl5AgMBAAECggEAGU8TlqQGmAbYEc8gGwvC5U9NtyCAGIQHtzTXM4XrVAlz
kuwQ6MC59JReP5TUg3+DNEC6TSAdfceIiPy3owLeUgXuJKQ1T859dx95LygyBLbP
k1W3BmSIQK7/Uoy8NjuJ4lt6/s2lZwhwfhnvGLrPuEpGpe3f/n5e31I5VM81apXN
Yc8pUT2DnL8UzvoAiePCnntHQGCBfOolSpKHgvqGvQTk59NlfwFNzVEHbDxlFgWP
s5URly5cd/ttak22hD0QE+M3uAJqb/JfT9oNN+0ZPc0hXt3XeySK4NAFyA41Sj+/
0mJjNhSH4psJGAlDADkPD4cIub8owpRkPOHH6cP3UQKBgQDxYqG0eAogO81bCFSU
a42KpGlVCO/Tvq3Z33SgX24Ul0ttc6n8afUcWHAPPhFp0fTWG/Z/KtWG0HaP1/Kz
Rx9O/I5sPTz6g8wqxNTa0qj7hJh+SyHXqBQEoKyTL/wtF2F+H+rYWZZ+O58zYqPx
jZLBTJXCl/ajopxppNl0CTCZOwKBgQDGe8hrEwmLrirHqKMlE3zssjS3dCwgvchA
FeJIOC+34m6T1iTxgIMk0k1f7xp/rt8vTJK7Hmaq7Sw6v4Y6JaWg6s2lcTN/xzGB
pWvKvwNJO4PiRSxNX5BgUHBF7+WDGp/ZxQAdoMBsQnvC4aW9rElFCgrgV6qJvmHg
wwt8Lfyc2wKBgQDh+2/eD7+TG8mOXwcoCGTzliaSmJJGTy5dWcjK12ysVFQmPPG4
QM5bYiRO8NHGmuw3guhLd6N92i4VTpuF4aDbBrCjftVWxwreQ3XvAud2yVUmb1pY
lp9fEble9r6EzG3WcTUgpQayWUkbB07qtprc4sEV88TQv0zlzpJSAsR/vwKBgEM+
ToEQGwzKfc3UsSjveERMf5WjcwvIoB4uC9KBzpDS0rmdNDjpXATOhs44mFanrQ8+
NvvT6d6AqZphppzugjWJNxCU0Gi62Gfe3iz7F6bo1d6DpuWzuZsXxWG8S5pmG7/Q
gSCIhIho4br9bYRb6RrNsy+cI7e02z4ldi+k+M8/AoGBAMwMxbaFc00ltVtF0v0H
E3APRQb5gKo8jxeaokm99bD7/ZRzF1QDPrqWjvXccpAKOEaLN9O3WslmH2TM+1oa
wyt+fuFYZw2E72dUunPc0YOf41F3NpRl3i50q1ewsGS72sq8YEMIFr/6MUG09Ht5
5ELK7eqbqsf197lNOYFUFE17
-----END PRIVATE KEY-----
#!/bin/bash
# Hermes Node Agent — Universal Installer (user-space, no root)
set -e
GREEN="\033[0;32m"; YELLOW="\033[1;33m"; RED="\033[0;31m"; CYAN="\033[0;36m"; NC="\033[0m"
echo ""
BANNER="
╔══════════════════════════════════════════════════════════╗
║ HERMES NODE AGENT — UNIVERSAL INSTALLER ║
║ User-space (no root), optional tools, configurable ║
╚══════════════════════════════════════════════════════════╝"
echo -e "$CYAN$BANNER$NC"
# ── Prerequisites ───────────────────────────────────────────────────────
echo -e "${CYAN}[1/6] Prerequisites...${NC}"
command -v python3 &>/dev/null || { echo -e "${RED}ERROR: Python 3 required${NC}"; exit 1; }
python3 -m pip install --user "websockets>=16.0" 2>/dev/null || true
python3 -c "import websockets" 2>/dev/null || {
command -v apt-get &>/dev/null && sudo apt-get install -y python3-websockets 2>/dev/null || true
}
python3 -c "import websockets" 2>/dev/null || { echo -e "${RED}ERROR: websockets required${NC}"; exit 1; }
echo -e " ${GREEN}✓ OK${NC}"
# ── Dependencies hints ────────────────────────────────────────────────────
echo ""
echo -e "${CYAN}Optional tool dependencies:${NC}"
echo " browser control: Chrome/Edge extension (separate repo)"
echo " computer control: xdotool, import (ImageMagick) — apt install x11-utils imagemagick"
echo ""
# ── Configuration ──────────────────────────────────────────────────────────
echo -e "${CYAN}[2/6] Configuration${NC}"
read -p " Node name [$(hostname)]: " NODE_NAME; NODE_NAME=${NODE_NAME:-$(hostname)}
read -p " Gateway host [lisa]: " GW_HOST; GW_HOST=${GW_HOST:-"lisa"}
read -p " Gateway port [8765]: " GW_PORT; GW_PORT=${GW_PORT:-"8765"}
echo ""
echo -e "${YELLOW}Your gateway admin should provide the node token.${NC}"
read -s -p " Gateway token: " GW_TOKEN; echo ""
[ -z "$GW_TOKEN" ] && { echo -e "${RED}Token required${NC}"; exit 1; }
SEXEC_DEF="$HOME/.openclaw/skills/sexec/sexec.sh"
read -p " sexec.sh path [$SEXEC_DEF]: " SEXEC_IN; SEXEC_PATH=${SEXEC_IN:-$SEXEC_DEF}
# ── Tool selection ──────────────────────────────────────────────────────────
echo ""
echo -e "${CYAN}[3/6] Optional tools${NC}"
read -p " Enable browser control? (y/n): " BROW_YN
ENABLE_BROWSER=false; if [[ "$BROW_YN" =~ ^[Yy] ]]; then ENABLE_BROWSER=true; fi
read -p " Enable computer_control? (y/n): " CC_YN
ENABLE_CC=false; if [[ "$CC_YN" =~ ^[Yy] ]]; then ENABLE_CC=true; fi
# ── sexec permissions quick-edit ───────────────────────────────────────────
echo ""
echo -e "${CYAN}[4/6] sexec permissions (regex patterns)${NC}"
echo " Leave blank to keep default (ask for everything)"
read -p " Allow patterns (comma separated): " ALLOW_IN
read -p " Deny patterns (comma separated): " DENY_IN
read -p " Ask patterns (comma separated, default '.*'): " ASK_IN
# Build JSON permissions string
if [ -n "$ALLOW_IN" ] || [ -n "$DENY_IN" ] || [ -n "$ASK_IN" ]; then
PERMS_JSON="{"
COMMA=""
if [ -n "$ALLOW_IN" ]; then
CLEAN=$(echo "$ALLOW_IN" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed 's/,/","/g')
PERMS_JSON="$PERMS_JSON\"allow\":[\"$CLEAN\"]"
COMMA=","
fi
if [ -n "$DENY_IN" ]; then
CLEAN=$(echo "$DENY_IN" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed 's/,/","/g')
PERMS_JSON="$PERMS_JSON$COMMA\"deny\":[\"$CLEAN\"]"
COMMA=","
fi
if [ -n "$ASK_IN" ]; then
CLEAN=$(echo "$ASK_IN" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed 's/,/","/g')
PERMS_JSON="$PERMS_JSON$COMMA\"ask\":[\"$CLEAN\"]"
else
PERMS_JSON="$PERMS_JSON$COMMA\"ask\":[\".*\"]"
fi
PERMS_JSON="$PERMS_JSON}"
else
PERMS_JSON=""
fi
# ── Confirm ────────────────────────────────────────────────────────────────
echo ""
echo -e "${CYAN}Summary:${NC}"
echo " Node: $NODE_NAME"
echo " Gateway: wss://$GW_HOST:$GW_PORT"
echo " Token: ${GW_TOKEN:0:12}..."
echo " sexec: $SEXEC_PATH"
BROWSER_TOOL=$([ "$ENABLE_BROWSER" = true ] && echo "YES" || echo "NO")
echo " Browser: $BROWSER_TOOL"
CC_TOOL=$([ "$ENABLE_CC" = true ] && echo "YES" || echo "NO")
echo " Computer ctrl: $CC_TOOL"
if [ -n "$PERMS_JSON" ] && [ "$PERMS_JSON" != "{}" ]; then
echo " sexec perms: $PERMS_JSON"
fi
read -p " Proceed? (yes/no): " CONFIRM
[ "$CONFIRM" = "yes" ] || { echo "Cancelled."; exit 0; }
# ── 5. Write config ────────────────────────────────────────────────────────
echo ""
echo -e "${CYAN}[5/6] Writing configuration...${NC}"
CFG_DIR="$HOME/.config/hermes-node"; mkdir -p "$CFG_DIR"
CFG="$CFG_DIR/config.json"
cat > "$CFG" <<ENDCFG
{
"gateway_url": "wss://$GW_HOST:$GW_PORT",
"node_name": "$NODE_NAME",
"token": "$GW_TOKEN",
"sexec_path": "$SEXEC_PATH",
"reconnect_interval": 5,
"heartbeat_interval": 30,
"enable_browser": $ENABLE_BROWSER,
"enable_computer_control": $ENABLE_CC,
"permissions": $PERMS_JSON
}
ENDCFG
chmod 600 "$CFG"
echo -e " ${GREEN}$CFG${NC}"
# ── 6. Install agent ────────────────────────────────────────────────────────
echo ""
echo -e "${CYAN}[6/6] Installing agent...${NC}"
mkdir -p /tmp/hna_install
echo "{agent_b64}" | base64 -d > /tmp/hna_install/hermes_node_agent.py
python3 -m py_compile /tmp/hna_install/hermes_node_agent.py 2>/dev/null || {{
echo -e "${RED}✗ Agent validation failed${NC}"; exit 1;
}}
mkdir -p "$HOME/.local/bin"
cp /tmp/hna_install/hermes_node_agent.py "$HOME/.local/bin/hermes-node-agent"
chmod 755 "$HOME/.local/bin/hermes-node-agent"
echo -e " ${GREEN}$HOME/.local/bin/hermes-node-agent${NC}"
# Optional system-wide if sudo available
if command -v sudo &>/dev/null && [ "$EUID" -ne 0 ]; then
sudo cp /tmp/hna_install/hermes_node_agent.py /usr/local/bin/hermes-node-agent 2>/dev/null && \
sudo chmod 755 /usr/local/bin/hermes-node-agent && \
echo -e " ${YELLOW}Also installed to /usr/local/bin${NC}"
fi
# Done
echo ""
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} ✓ INSTALLED${NC}"
echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}"
echo ""
echo "Agent: $HOME/.local/bin/hermes-node-agent"
echo "Config: $CFG"
echo ""
TOOL_LIST="exec"
[ "$ENABLE_BROWSER" = true ] && TOOL_LIST="$TOOL_LIST + browser"
[ "$ENABLE_CC" = true ] && TOOL_LIST="$TOOL_LIST + computer_control"
echo "Enabled tools: $TOOL_LIST"
echo ""
echo "Start: $HOME/.local/bin/hermes-node-agent"
echo ""
This diff is collapsed.
<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='YOUR_EXTENSION_ID_HERE'>
<updatecheck codebase='https://your-server.com/hermes-browser-node-agent.crx' version='2.0' />
</app>
</gupdate>
\ No newline at end of file
This diff is collapsed.
# Hermes Browser Node Agent Extension
A Chrome/Edge extension that turns any browser into a Hermes node agent, enabling remote browser automation without installing any software on the host machine.
## Architecture
```
Browser Extension (Service Worker)
↓ WebSocket (WSS)
Hermes Gateway Plugin
Hermes Agent / User
```
The extension connects directly to your Hermes gateway and registers as a browser-type node with capabilities:
- `browser` - Tab management, navigation, screenshots
- `tabs` - Create, close, list tabs
- `scripting` - JavaScript injection and execution
- `inject` - Page-level API access via window.HermesAgent
## Features
### 3-Layer API
**Layer 1: High-Level Commands**
- `list_tabs` - List all open tabs
- `create_tab` - Open new tab
- `navigate` - Go to URL
- `close_tab` - Close tab
- `screenshot` - Capture visible area
- `get_content` - Get page HTML
- `get_title` - Get page title
- `click` - Click element by selector
- `fill` - Fill form field
- `evaluate` - Execute JavaScript and return result
**Layer 2: Inject (Page Context)**
- `inject_script` - Execute arbitrary JavaScript in page
- `inject_file` - Load external script file
- Access to `window.HermesAgent` API from page JavaScript
**Layer 3: CDP (Future)**
- Chrome DevTools Protocol access (requires debugger permission)
### Page-Level API
When injected, pages get `window.HermesAgent` with:
- `getPageInfo()` - URL, title, viewport, scroll position
- `waitForSelector(selector, timeout)` - Wait for element
- `waitForVisible(selector, timeout)` - Wait for visible element
- `fillField(selector, value)` - Fill form
- `clickElement(selector)` - Click element
- `getText(selector)` - Get element text
- `getAttribute(selector, attr)` - Get attribute
- `queryAll(selector)` - Query all matching elements
- `xpath(expression)` - XPath query
- `scrollTo(selector)` - Scroll to element
- `getCookies()` - Get document cookies
- `getLocalStorage()` - Get localStorage
- `getSessionStorage()` - Get sessionStorage
- `observeMutations(selector, callback)` - Watch DOM changes
- `interceptFetch(callback)` - Monitor fetch requests
- `interceptXHR(callback)` - Monitor XHR requests
## Installation
### 1. Load Extension in Chrome
1. Open Chrome and go to `chrome://extensions/`
2. Enable "Developer mode" (top right)
3. Click "Load unpacked"
4. Select the `hermes_browser_extension` directory
5. Extension icon appears in toolbar
### 2. Configure Connection
1. Click the Hermes extension icon
2. Enter your gateway details:
- **Gateway URL**: `wss://your-gateway-host:8765`
- **Node Name**: Unique identifier (e.g., `browser-laptop`)
- **Token**: Authentication token from gateway config
3. Click "Save & Connect"
4. Status should show "● Connected"
### 3. Get Token from Gateway
On your Hermes gateway machine:
```bash
# Token is in gateway config
cat /etc/hermes-node-gateway/config.json | grep -A 5 tokens
# Or generate a new one
openssl rand -hex 32
```
Add the token to gateway config:
```json
{
"tokens": {
"browser-laptop": "your-new-token-here"
}
}
```
Restart gateway to apply.
## Usage from Hermes
Once connected, the browser node appears in Hermes:
```python
# List connected nodes
node_list()
# Create new tab and navigate
browser_control(
node_name="browser-laptop",
command="create_tab",
params={"url": "https://github.com"}
)
# Take screenshot
browser_control(
node_name="browser-laptop",
command="screenshot",
page_id="tab_123",
params={"format": "png"}
)
# Execute JavaScript
browser_control(
node_name="browser-laptop",
command="evaluate",
page_id="tab_123",
params={"expression": "document.querySelectorAll('a').length"}
)
# Fill form and submit
browser_control(
node_name="browser-laptop",
command="fill",
page_id="tab_123",
params={"selector": "input[name=q]", "value": "hermes agent"}
)
browser_control(
node_name="browser-laptop",
command="click",
page_id="tab_123",
params={"selector": "button[type=submit]"}
)
```
## Security
- **WSS (TLS)**: Always use `wss://` for encrypted connections
- **Token Auth**: Each node requires a unique token
- **Same-Origin**: Content scripts respect same-origin policy
- **No Shell Access**: Browser nodes cannot execute shell commands
- **Permissions**: Extension only requests necessary Chrome APIs
## Limitations
- **No CDP Access**: Chrome DevTools Protocol requires `chrome.debugger` permission (not yet implemented)
- **No Playwright**: Extension uses native Chrome APIs, not Playwright
- **Visible Tabs Only**: Screenshots only capture visible tab area
- **Service Worker**: Background script is a service worker (may sleep when inactive)
## Troubleshooting
**Extension won't connect:**
- Check gateway URL is correct (`wss://` not `ws://`)
- Verify token matches gateway config
- Check gateway is running: `ss -tlnp | grep 8765`
- Check browser console: Right-click extension → Inspect → Console
**Commands fail:**
- Verify node is connected: `node_list()`
- Check page_id is correct: `browser_control(node_name="...", command="list_tabs")`
- Some sites block scripting (CSP headers)
**Extension disappears:**
- Service workers sleep after inactivity
- Extension auto-reconnects on next activity
- Check background page: `chrome://extensions/` → Details → Inspect views: service worker
## Development
**Reload extension after changes:**
1. Go to `chrome://extensions/`
2. Click reload icon on Hermes extension card
3. Check service worker console for errors
**Debug background script:**
1. `chrome://extensions/` → Hermes extension → Details
2. Click "Inspect views: service worker"
3. Console shows connection logs
**Debug content script:**
1. Open any page
2. F12 → Console
3. Look for `[Hermes] Content script loaded`
4. Test: `window.HermesAgent.getPageInfo()`
## Files
- `manifest.json` - Extension manifest (MV3)
- `background.js` - Service worker, WebSocket client, command handler
- `popup.html` - Configuration UI
- `popup.js` - Popup controller
- `content-inject.js` - Content script bridge
- `injected.js` - Page-level API (window.HermesAgent)
- `icons/` - Extension icons (16x16, 48x48, 128x128)
## Protocol
Extension implements the same WebSocket protocol as node agents:
**Registration:**
```json
{
"type": "register",
"node_name": "browser-laptop",
"version": "2.0",
"capabilities": ["browser", "tabs", "scripting", "inject"],
"platform": "browser_extension"
}
```
**Commands:**
```json
{
"type": "browser_control",
"id": "cmd-abc123",
"command": "navigate",
"layer": "high_level",
"page_id": "tab_456",
"params": {"url": "https://example.com"}
}
```
**Responses:**
```json
{
"type": "browser_control_response",
"id": "cmd-abc123",
"result": "ok",
"url": "https://example.com/",
"title": "Example Domain"
}
```
## License
Part of Hermes Agent project.
This diff is collapsed.
/**
* CDP (Chrome DevTools Protocol) Support
*
* Adds chrome.debugger API support for full CDP access.
* Requires additional permission: "debugger"
*/
// CDP session management
const cdpSessions = new Map(); // tabId -> debugger session
/**
* Attach debugger to tab for CDP access
*/
async function attachDebugger(tabId) {
if (cdpSessions.has(tabId)) {
return cdpSessions.get(tabId);
}
try {
await chrome.debugger.attach({ tabId }, '1.3');
cdpSessions.set(tabId, { tabId, attached: true });
console.log('[Hermes CDP] Attached to tab', tabId);
return cdpSessions.get(tabId);
} catch (error) {
console.error('[Hermes CDP] Failed to attach:', error);
throw new Error(`CDP attach failed: ${error.message}`);
}
}
/**
* Detach debugger from tab
*/
async function detachDebugger(tabId) {
if (!cdpSessions.has(tabId)) {
return;
}
try {
await chrome.debugger.detach({ tabId });
cdpSessions.delete(tabId);
console.log('[Hermes CDP] Detached from tab', tabId);
} catch (error) {
console.error('[Hermes CDP] Failed to detach:', error);
}
}
/**
* Send CDP command
*/
async function sendCDPCommand(tabId, method, params = {}) {
// Ensure debugger is attached
await attachDebugger(tabId);
try {
const result = await chrome.debugger.sendCommand(
{ tabId },
method,
params
);
return { success: true, result };
} catch (error) {
console.error('[Hermes CDP] Command failed:', method, error);
return { success: false, error: error.message };
}
}
/**
* Execute CDP commands (called from main handler)
*/
async function executeCDPCommand(command, page_id, params) {
const tabId = page_id ? parseInt(page_id.replace('tab_', '')) : null;
if (!tabId) {
return { success: false, error: 'page_id required for CDP commands' };
}
// Common CDP commands
switch (command) {
case 'attach':
await attachDebugger(tabId);
return { success: true, attached: true };
case 'detach':
await detachDebugger(tabId);
return { success: true, detached: true };
default:
// Generic CDP command
return await sendCDPCommand(tabId, command, params);
}
}
/**
* Listen for debugger detach events
*/
chrome.debugger.onDetach.addListener((source, reason) => {
const tabId = source.tabId;
console.log('[Hermes CDP] Debugger detached from tab', tabId, 'reason:', reason);
cdpSessions.delete(tabId);
});
/**
* Listen for debugger events (for event subscriptions)
*/
chrome.debugger.onEvent.addListener((source, method, params) => {
// Could forward CDP events to gateway if needed
console.log('[Hermes CDP] Event:', method, params);
});
// Export for use in background.js
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
attachDebugger,
detachDebugger,
sendCDPCommand,
executeCDPCommand
};
}
console.log('[Hermes CDP] Module loaded');
/**
* Content script injected into all pages
* Provides a communication bridge between page context and extension
*/
// Inject the page-level API script
const script = document.createElement('script');
script.src = chrome.runtime.getURL('injected.js');
script.onload = function() {
this.remove();
};
(document.head || document.documentElement).appendChild(script);
// Listen for messages from injected script
window.addEventListener('message', (event) => {
// Only accept messages from same origin
if (event.source !== window) return;
if (event.data && event.data.type === 'hermes_from_page') {
// Forward to background script
chrome.runtime.sendMessage({
type: 'page_message',
data: event.data.payload
});
}
});
// Listen for messages from background script
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'execute_in_page') {
// Execute code in page context
try {
const result = eval(msg.code);
sendResponse({ success: true, result });
} catch (error) {
sendResponse({ success: false, error: error.message });
}
}
return true; // async response
});
console.log('[Hermes] Content script loaded');
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="64" cy="64" r="60" fill="#4CAF50" opacity="0.1"/>
<!-- Brain outline -->
<path d="M 40 45 Q 35 35, 45 30 Q 55 25, 60 30 Q 65 25, 75 30 Q 85 35, 80 45 Q 85 50, 85 60 Q 85 70, 80 75 Q 85 80, 80 90 Q 75 95, 65 95 Q 60 100, 55 95 Q 45 95, 40 90 Q 35 80, 40 75 Q 35 70, 35 60 Q 35 50, 40 45 Z"
fill="none" stroke="#4CAF50" stroke-width="3" stroke-linecap="round"/>
<!-- Brain details -->
<path d="M 50 40 Q 55 45, 50 50" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 70 40 Q 65 45, 70 50" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 45 60 Q 50 65, 45 70" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 75 60 Q 70 65, 75 70" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 55 80 Q 60 75, 65 80" fill="none" stroke="#4CAF50" stroke-width="2"/>
<!-- Network nodes -->
<circle cx="30" cy="64" r="4" fill="#2196F3"/>
<circle cx="98" cy="64" r="4" fill="#2196F3"/>
<circle cx="64" cy="30" r="4" fill="#2196F3"/>
<circle cx="64" cy="98" r="4" fill="#2196F3"/>
<!-- Connection lines -->
<line x1="34" y1="64" x2="40" y2="64" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="80" y1="64" x2="94" y2="64" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="64" y1="34" x2="64" y2="40" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="64" y1="90" x2="64" y2="94" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<!-- Center dot -->
<circle cx="64" cy="64" r="3" fill="#FF5722"/>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="64" cy="64" r="60" fill="#4CAF50" opacity="0.1"/>
<!-- Brain outline -->
<path d="M 40 45 Q 35 35, 45 30 Q 55 25, 60 30 Q 65 25, 75 30 Q 85 35, 80 45 Q 85 50, 85 60 Q 85 70, 80 75 Q 85 80, 80 90 Q 75 95, 65 95 Q 60 100, 55 95 Q 45 95, 40 90 Q 35 80, 40 75 Q 35 70, 35 60 Q 35 50, 40 45 Z"
fill="none" stroke="#4CAF50" stroke-width="3" stroke-linecap="round"/>
<!-- Brain details -->
<path d="M 50 40 Q 55 45, 50 50" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 70 40 Q 65 45, 70 50" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 45 60 Q 50 65, 45 70" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 75 60 Q 70 65, 75 70" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 55 80 Q 60 75, 65 80" fill="none" stroke="#4CAF50" stroke-width="2"/>
<!-- Network nodes -->
<circle cx="30" cy="64" r="4" fill="#2196F3"/>
<circle cx="98" cy="64" r="4" fill="#2196F3"/>
<circle cx="64" cy="30" r="4" fill="#2196F3"/>
<circle cx="64" cy="98" r="4" fill="#2196F3"/>
<!-- Connection lines -->
<line x1="34" y1="64" x2="40" y2="64" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="80" y1="64" x2="94" y2="64" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="64" y1="34" x2="64" y2="40" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="64" y1="90" x2="64" y2="94" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<!-- Center dot -->
<circle cx="64" cy="64" r="3" fill="#FF5722"/>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="64" cy="64" r="60" fill="#4CAF50" opacity="0.1"/>
<!-- Brain outline -->
<path d="M 40 45 Q 35 35, 45 30 Q 55 25, 60 30 Q 65 25, 75 30 Q 85 35, 80 45 Q 85 50, 85 60 Q 85 70, 80 75 Q 85 80, 80 90 Q 75 95, 65 95 Q 60 100, 55 95 Q 45 95, 40 90 Q 35 80, 40 75 Q 35 70, 35 60 Q 35 50, 40 45 Z"
fill="none" stroke="#4CAF50" stroke-width="3" stroke-linecap="round"/>
<!-- Brain details -->
<path d="M 50 40 Q 55 45, 50 50" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 70 40 Q 65 45, 70 50" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 45 60 Q 50 65, 45 70" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 75 60 Q 70 65, 75 70" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 55 80 Q 60 75, 65 80" fill="none" stroke="#4CAF50" stroke-width="2"/>
<!-- Network nodes -->
<circle cx="30" cy="64" r="4" fill="#2196F3"/>
<circle cx="98" cy="64" r="4" fill="#2196F3"/>
<circle cx="64" cy="30" r="4" fill="#2196F3"/>
<circle cx="64" cy="98" r="4" fill="#2196F3"/>
<!-- Connection lines -->
<line x1="34" y1="64" x2="40" y2="64" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="80" y1="64" x2="94" y2="64" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="64" y1="34" x2="64" y2="40" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="64" y1="90" x2="64" y2="94" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<!-- Center dot -->
<circle cx="64" cy="64" r="3" fill="#FF5722"/>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<!-- Background circle -->
<circle cx="64" cy="64" r="60" fill="#4CAF50" opacity="0.1"/>
<!-- Brain outline -->
<path d="M 40 45 Q 35 35, 45 30 Q 55 25, 60 30 Q 65 25, 75 30 Q 85 35, 80 45 Q 85 50, 85 60 Q 85 70, 80 75 Q 85 80, 80 90 Q 75 95, 65 95 Q 60 100, 55 95 Q 45 95, 40 90 Q 35 80, 40 75 Q 35 70, 35 60 Q 35 50, 40 45 Z"
fill="none" stroke="#4CAF50" stroke-width="3" stroke-linecap="round"/>
<!-- Brain details -->
<path d="M 50 40 Q 55 45, 50 50" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 70 40 Q 65 45, 70 50" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 45 60 Q 50 65, 45 70" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 75 60 Q 70 65, 75 70" fill="none" stroke="#4CAF50" stroke-width="2"/>
<path d="M 55 80 Q 60 75, 65 80" fill="none" stroke="#4CAF50" stroke-width="2"/>
<!-- Network nodes -->
<circle cx="30" cy="64" r="4" fill="#2196F3"/>
<circle cx="98" cy="64" r="4" fill="#2196F3"/>
<circle cx="64" cy="30" r="4" fill="#2196F3"/>
<circle cx="64" cy="98" r="4" fill="#2196F3"/>
<!-- Connection lines -->
<line x1="34" y1="64" x2="40" y2="64" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="80" y1="64" x2="94" y2="64" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="64" y1="34" x2="64" y2="40" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<line x1="64" y1="90" x2="64" y2="94" stroke="#2196F3" stroke-width="2" opacity="0.5"/>
<!-- Center dot -->
<circle cx="64" cy="64" r="3" fill="#FF5722"/>
</svg>
\ No newline at end of file
/**
* Injected script - runs in page context
* Provides window.HermesAgent API for page-level JavaScript
*/
(function() {
'use strict';
// Prevent double injection
if (window.HermesAgent) {
console.log('[Hermes] API already injected');
return;
}
/**
* Hermes Agent API
* Available to page JavaScript as window.HermesAgent
*/
window.HermesAgent = {
version: '2.0',
/**
* Send message to extension background
*/
sendToExtension: function(payload) {
window.postMessage({
type: 'hermes_from_page',
payload: payload
}, '*');
},
/**
* Get page information
*/
getPageInfo: function() {
return {
url: window.location.href,
title: document.title,
domain: window.location.hostname,
protocol: window.location.protocol,
referrer: document.referrer,
timestamp: Date.now(),
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
scroll: {
x: window.scrollX,
y: window.scrollY
}
};
},
/**
* Wait for selector to appear
*/
waitForSelector: function(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const start = Date.now();
const check = () => {
const el = document.querySelector(selector);
if (el) {
resolve(el);
} else if (Date.now() - start > timeout) {
reject(new Error(`Timeout waiting for selector: ${selector}`));
} else {
requestAnimationFrame(check);
}
};
check();
});
},
/**
* Wait for element to be visible
*/
waitForVisible: function(selector, timeout = 5000) {
return new Promise((resolve, reject) => {
const start = Date.now();
const check = () => {
const el = document.querySelector(selector);
if (el && el.offsetParent !== null) {
resolve(el);
} else if (Date.now() - start > timeout) {
reject(new Error(`Timeout waiting for visible: ${selector}`));
} else {
requestAnimationFrame(check);
}
};
check();
});
},
/**
* Fill form field
*/
fillField: function(selector, value) {
const el = document.querySelector(selector);
if (!el) {
throw new Error(`Element not found: ${selector}`);
}
el.value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
return true;
},
/**
* Click element
*/
clickElement: function(selector) {
const el = document.querySelector(selector);
if (!el) {
throw new Error(`Element not found: ${selector}`);
}
el.click();
return true;
},
/**
* Get element text
*/
getText: function(selector) {
const el = document.querySelector(selector);
if (!el) {
throw new Error(`Element not found: ${selector}`);
}
return el.textContent.trim();
},
/**
* Get element attribute
*/
getAttribute: function(selector, attr) {
const el = document.querySelector(selector);
if (!el) {
throw new Error(`Element not found: ${selector}`);
}
return el.getAttribute(attr);
},
/**
* Query all elements
*/
queryAll: function(selector) {
return Array.from(document.querySelectorAll(selector));
},
/**
* Execute XPath query
*/
xpath: function(expression, contextNode = document) {
const result = document.evaluate(
expression,
contextNode,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
const nodes = [];
for (let i = 0; i < result.snapshotLength; i++) {
nodes.push(result.snapshotItem(i));
}
return nodes;
},
/**
* Scroll to element
*/
scrollTo: function(selector) {
const el = document.querySelector(selector);
if (!el) {
throw new Error(`Element not found: ${selector}`);
}
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
return true;
},
/**
* Get all cookies (via document.cookie)
*/
getCookies: function() {
return document.cookie.split(';').map(c => {
const [name, value] = c.trim().split('=');
return { name, value };
});
},
/**
* Get localStorage
*/
getLocalStorage: function() {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
data[key] = localStorage.getItem(key);
}
return data;
},
/**
* Get sessionStorage
*/
getSessionStorage: function() {
const data = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
data[key] = sessionStorage.getItem(key);
}
return data;
},
/**
* Monitor DOM mutations
*/
observeMutations: function(selector, callback, options = {}) {
const target = document.querySelector(selector);
if (!target) {
throw new Error(`Element not found: ${selector}`);
}
const observer = new MutationObserver(callback);
observer.observe(target, {
childList: true,
subtree: true,
attributes: true,
characterData: true,
...options
});
return observer;
},
/**
* Intercept fetch requests (monkey-patch)
*/
interceptFetch: function(callback) {
const originalFetch = window.fetch;
window.fetch = function(...args) {
callback({ type: 'fetch', args });
return originalFetch.apply(this, args);
};
return function restore() {
window.fetch = originalFetch;
};
},
/**
* Intercept XHR requests
*/
interceptXHR: function(callback) {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
this._hermesMethod = method;
this._hermesUrl = url;
return originalOpen.apply(this, [method, url, ...rest]);
};
XMLHttpRequest.prototype.send = function(...args) {
callback({
type: 'xhr',
method: this._hermesMethod,
url: this._hermesUrl,
data: args[0]
});
return originalSend.apply(this, args);
};
return function restore() {
XMLHttpRequest.prototype.open = originalOpen;
XMLHttpRequest.prototype.send = originalSend;
};
}
};
// Notify that API is ready
console.log('[Hermes] Page API injected - window.HermesAgent available');
// Dispatch custom event
window.dispatchEvent(new CustomEvent('hermes:ready', {
detail: { version: window.HermesAgent.version }
}));
})();
#!/bin/bash
#
# Hermes Browser Node Agent - Installation Script
# This script installs the Hermes Browser Extension for Chrome/Edge
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
EXTENSION_DIR="${SCRIPT_DIR}/hermes_browser_extension"
echo "🧠 Hermes Browser Node Agent - Installation"
echo "=========================================="
echo ""
# Check OS
OS="$(uname -s)"
case "${OS}" in
Linux*) MACHINE=Linux;;
Darwin*) MACHINE=Mac;;
CYGWIN*) MACHINE=Cygwin;;
MINGW*) MACHINE=MinGw;;
*) MACHINE="UNKNOWN:${OS}"
esac
echo "Detected OS: ${MACHINE}"
if [ "${MACHINE}" != "Linux" ]; then
echo "⚠️ This script is designed for Linux. For other OSes, please load the extension manually."
fi
# Check if Chrome or Chromium is installed
if command -v google-chrome &> /dev/null; then
CHROME="google-chrome"
elif command -v chrome &> /dev/null; then
CHROME="chrome"
elif command -v chromium &> /dev/null; then
CHROME="chromium"
elif command -v chromium-browser &> /dev/null; then
CHROME="chromium-browser"
else
echo "❌ Could not find Chrome/Chromium installed"
echo " Please install Chrome or Chromium first:"
echo " - Ubuntu/Debian: sudo apt install chromium-browser"
echo " - Fedora: sudo dnf install chromium"
echo " - Arch: sudo pacman -S chromium"
exit 1
fi
echo "✓ Found browser: ${CHROME}"
echo ""
# Check extension directory
if [ ! -d "${EXTENSION_DIR}" ]; then
echo "❌ Extension directory not found: ${EXTENSION_DIR}"
exit 1
fi
echo "✓ Extension directory: ${EXTENSION_DIR}"
echo ""
# For Linux, we can't auto-install (needs manual chrome://extensions)
# But we can provide instructions and help with config
echo "📦 INSTALLATION INSTRUCTIONS"
echo "============================"
echo ""
echo "1. Open your browser and go to:"
echo " chrome://extensions/"
echo ""
echo "2. Enable 'Developer mode' (toggle in top right)"
echo ""
echo "3. Click 'Load unpacked'"
echo ""
echo "4. Select directory:"
echo " ${EXTENSION_DIR}"
echo ""
echo "5. The Hermes extension icon should appear in your toolbar"
echo ""
# Ask if user wants to open chrome://extensions automatically
read -p "Open chrome://extensions in your browser now? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
if [ -n "${DISPLAY}" ]; then
"${CHROME}" "chrome://extensions/" 2>/dev/null || xdg-open "chrome://extensions/"
else
echo "No display detected - please open manually"
fi
fi
echo ""
echo "✅ Installation complete!"
echo ""
echo "🎯 NEXT STEPS:"
echo "1. Click the extension icon in toolbar"
echo "2. Enter your gateway details:"
echo " - Gateway URL: wss://your-gateway:8765"
echo " - Node Name: browser-laptop"
echo " - Token: (contact admin or check /etc/hermes-node-gateway/config.json)"
echo "3. Click 'Save & Connect'"
echo ""
echo "📚 See README.md for full usage guide:"
echo " ${EXTENSION_DIR}/README.md"
{
"manifest_version": 3,
"name": "Hermes Browser Node Agent",
"version": "2.0",
"description": "Browser-based node agent for Hermes. Connects directly to Hermes Gateway via WebSocket, enabling remote browser automation, CDP access, and JavaScript injection without any local software installation.",
"permissions": [
"storage",
"tabs",
"scripting",
"activeTab",
"webNavigation",
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess",
"alarms",
"debugger"
],
"host_permissions": [
"http://*/*",
"https://*/*"
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-inject.js"],
"run_at": "document_start",
"all_frames": true
}
],
"web_accessible_resources": [
{
"resources": ["injected.js", "content-inject.js"],
"matches": ["<all_urls>"]
}
]
}
#!/usr/bin/env python3
"""
Package Hermes Browser Extension as .crx for distribution
Creates a signed Chrome extension package that can be distributed
and installed without developer mode.
"""
import os
import sys
import subprocess
import zipfile
from pathlib import Path
import shutil
EXTENSION_DIR = Path(__file__).parent
OUTPUT_DIR = EXTENSION_DIR.parent / 'dist'
EXTENSION_NAME = 'hermes-browser-node-agent'
def create_zip_package():
"""Create unsigned .zip package"""
print("📦 Creating ZIP package...")
OUTPUT_DIR.mkdir(exist_ok=True)
zip_path = OUTPUT_DIR / f'{EXTENSION_NAME}.zip'
# Files to include
include_patterns = [
'*.js',
'*.html',
'*.json',
'*.md',
'icons/*'
]
exclude_patterns = [
'__pycache__',
'*.pyc',
'.git',
'package.py',
'install.sh'
]
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for pattern in include_patterns:
for file in EXTENSION_DIR.glob(pattern):
if file.is_file():
arcname = file.relative_to(EXTENSION_DIR)
if not any(ex in str(arcname) for ex in exclude_patterns):
zf.write(file, arcname)
print(f" ✓ {arcname}")
size = zip_path.stat().st_size
print(f"\n✅ ZIP package created: {zip_path}")
print(f" Size: {size:,} bytes ({size/1024:.1f} KB)")
return zip_path
def create_crx_package(zip_path):
"""Create signed .crx package (requires Chrome)"""
print("\n🔐 Creating signed CRX package...")
# Check if Chrome is available
chrome_paths = [
'/usr/bin/google-chrome',
'/usr/bin/chromium',
'/usr/bin/chromium-browser',
'/opt/google/chrome/chrome',
shutil.which('google-chrome'),
shutil.which('chromium'),
shutil.which('chromium-browser')
]
chrome = None
for path in chrome_paths:
if path and Path(path).exists():
chrome = path
break
if not chrome:
print("⚠️ Chrome/Chromium not found - cannot create signed CRX")
print(" ZIP package can still be loaded in developer mode")
return None
print(f"✓ Found Chrome: {chrome}")
# Generate private key if it doesn't exist
key_path = OUTPUT_DIR / f'{EXTENSION_NAME}.pem'
crx_path = OUTPUT_DIR / f'{EXTENSION_NAME}.crx'
if not key_path.exists():
print(" Generating private key...")
# Chrome will generate key on first pack
# Pack extension
try:
cmd = [
chrome,
'--pack-extension=' + str(EXTENSION_DIR),
'--pack-extension-key=' + str(key_path) if key_path.exists() else ''
]
result = subprocess.run(
[c for c in cmd if c], # Filter empty strings
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
# Chrome creates .crx in parent directory
generated_crx = EXTENSION_DIR.parent / f'{EXTENSION_DIR.name}.crx'
if generated_crx.exists():
shutil.move(str(generated_crx), str(crx_path))
print(f"\n✅ CRX package created: {crx_path}")
print(f" Size: {crx_path.stat().st_size:,} bytes")
# Move key if generated
generated_key = EXTENSION_DIR.parent / f'{EXTENSION_DIR.name}.pem'
if generated_key.exists() and not key_path.exists():
shutil.move(str(generated_key), str(key_path))
print(f" Private key: {key_path}")
print(" ⚠️ Keep this key secure - needed for updates!")
return crx_path
print(f"⚠️ Chrome pack failed: {result.stderr}")
return None
except subprocess.TimeoutExpired:
print("⚠️ Chrome pack timed out")
return None
except Exception as e:
print(f"⚠️ Error creating CRX: {e}")
return None
def create_update_manifest(crx_path):
"""Create update manifest XML for auto-updates"""
if not crx_path or not crx_path.exists():
return
print("\n📝 Creating update manifest...")
# Read version from manifest.json
import json
manifest_path = EXTENSION_DIR / 'manifest.json'
with open(manifest_path) as f:
manifest = json.load(f)
version = manifest.get('version', '1.0')
# Create update XML
update_xml = f'''<?xml version='1.0' encoding='UTF-8'?>
<gupdate xmlns='http://www.google.com/update2/response' protocol='2.0'>
<app appid='YOUR_EXTENSION_ID_HERE'>
<updatecheck codebase='https://your-server.com/hermes-browser-node-agent.crx' version='{version}' />
</app>
</gupdate>'''
update_path = OUTPUT_DIR / 'update.xml'
update_path.write_text(update_xml)
print(f"✅ Update manifest: {update_path}")
print(" Update the appid and codebase URL before deploying")
def main():
print("=" * 60)
print("Hermes Browser Extension - Package Builder")
print("=" * 60)
print()
# Create ZIP package (always works)
zip_path = create_zip_package()
# Try to create CRX package (requires Chrome)
crx_path = create_crx_package(zip_path)
# Create update manifest
create_update_manifest(crx_path)
print("\n" + "=" * 60)
print("📦 DISTRIBUTION PACKAGES")
print("=" * 60)
print()
print(f"ZIP (developer mode): {OUTPUT_DIR / f'{EXTENSION_NAME}.zip'}")
if crx_path:
print(f"CRX (signed): {crx_path}")
print(f"Private key: {OUTPUT_DIR / f'{EXTENSION_NAME}.pem'}")
print()
print("📚 DISTRIBUTION OPTIONS:")
print()
print("1. Developer Mode (ZIP):")
print(" - Users load unpacked extension")
print(" - No signing required")
print(" - Best for internal deployment")
print()
print("2. Self-Hosted (CRX):")
print(" - Host .crx file on your server")
print(" - Users install via drag-and-drop")
print(" - Requires HTTPS")
print()
print("3. Chrome Web Store:")
print(" - Upload ZIP to Chrome Web Store Developer Dashboard")
print(" - Public or unlisted distribution")
print(" - $5 one-time developer fee")
print(" - URL: https://chrome.google.com/webstore/devconsole")
print()
if __name__ == '__main__':
main()
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hermes Browser Node Agent</title>
<style>
body {
width: 400px;
padding: 16px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
margin: 0;
}
h1 {
font-size: 18px;
margin: 0 0 16px 0;
color: #333;
}
.status {
padding: 12px;
border-radius: 6px;
margin-bottom: 16px;
font-weight: 500;
}
.status.connected {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.disconnected {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.connecting {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeaa7;
}
.form-group {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #555;
}
input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
input:focus {
outline: none;
border-color: #4CAF50;
}
button {
padding: 10px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin-right: 8px;
}
button.primary {
background: #4CAF50;
color: white;
}
button.primary:hover {
background: #45a049;
}
button.secondary {
background: #f1f1f1;
color: #333;
}
button.secondary:hover {
background: #e0e0e0;
}
button.danger {
background: #f44336;
color: white;
}
button.danger:hover {
background: #da190b;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.info {
font-size: 12px;
color: #666;
margin-top: 4px;
}
.actions {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #eee;
}
.node-info {
background: #f8f9fa;
padding: 12px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 13px;
}
.node-info div {
margin-bottom: 4px;
}
.node-info strong {
color: #555;
}
</style>
</head>
<body>
<h1>🧠 Hermes Browser Node Agent</h1>
<div id="status" class="status disconnected">
● Disconnected
</div>
<div id="node-info" class="node-info" style="display: none;">
<div><strong>Node Name:</strong> <span id="node-name">-</span></div>
<div><strong>Gateway:</strong> <span id="gateway-url">-</span></div>
<div><strong>Version:</strong> 2.0</div>
</div>
<div class="form-group">
<label for="gateway-input">Gateway URL</label>
<input type="text" id="gateway-input" placeholder="wss://your-gateway:8765" />
<div class="info">WebSocket URL of your Hermes gateway</div>
</div>
<div class="form-group">
<label for="node-name-input">Node Name</label>
<input type="text" id="node-name-input" placeholder="browser-laptop" />
<div class="info">Unique identifier for this browser node</div>
</div>
<div class="form-group">
<label for="token-input">Authentication Token</label>
<input type="password" id="token-input" placeholder="Your gateway token" />
<div class="info">Token from gateway config.json</div>
</div>
<div class="actions">
<button id="save-btn" class="primary">Save & Connect</button>
<button id="disconnect-btn" class="danger" style="display: none;">Disconnect</button>
</div>
<script src="popup.js"></script>
</body>
</html>
This diff is collapsed.
name: hermes-node-gateway
version: 1.0.0
description: "Execute commands on remote nodes via reverse-connected WebSocket (no SSH keys). Nodes initiate WSS connections to this gateway; all traffic flows through the Hermes process."
author: Lisa (Hermes AI)
kind: standalone
platforms:
- linux
provides_tools:
- node_list
- node_status
- node_exec
- browser_control
hooks: []
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