Commit 8892ed3a authored by Lisa's avatar Lisa

fix: repair browser control runtime and republish bundle

parent 2a9456fc
......@@ -11,7 +11,7 @@
**Version:** 1.0
**Date:** 2026-04-29
**Purpose:** Reverse-connection node execution with permission model, compatible with existing OpenClaw `sexec.sh` scripts.
**Purpose:** Reverse-connection node execution with an integrated node-local permission model enforced by the agent.
---
......@@ -48,14 +48,15 @@
│ └───────────────────────────┬──────────────────────┘ │
│ │ receives │
│ ┌───────────────────────────▼──────────────────────┐ │
│ │ Command Executor (sexec wrapper) │ │
│ │ - Runs: /path/to/sexec.sh run --command ... │ │
│ │ Integrated Command Executor │ │
│ │ - Evaluates allow/deny/ask rules │ │
│ │ - Executes approved commands directly │ │
│ │ - Streams stdout/stderr back │ │
│ │ - Returns exit code │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
Permission System (sexec.sh + config.json) - reused exactly
Permission System (config.json permissions block) - enforced by the agent
```
---
......@@ -72,8 +73,14 @@ All messages are JSON over WebSocket.
"type": "register",
"node_name": "sissy",
"version": "1.0",
"capabilities": ["exec", "sysinfo"],
"sexec_path": "/home/openclaw/.openclaw/skills/sexec/sexec.sh"
"capabilities": ["exec"],
"capability_info": {
"enable_browser": false,
"enable_computer_control": false,
"enable_desktop_observe": false,
"enable_audio_control": false,
"enable_camera_control": false
}
}
```
......@@ -121,7 +128,7 @@ All messages are JSON over WebSocket.
- `id`: Unique command ID for tracking
- `command`: Array of command + args (e.g., `["df", "-h"]`)
- `timeout`: Max execution time in seconds
- `approved`: If `true`, bypass "ask" list (user explicitly approved)
- `approved`: If `true`, bypass the node's `permissions.ask` rules (user explicitly approved)
### 4. Node → Gateway: Command Output (streaming)
......@@ -158,7 +165,7 @@ All messages are JSON over WebSocket.
### 6. Node → Gateway: Approval Required
**When command matches "ask" list:**
**When command matches the node's `permissions.ask` rules:**
```json
{
"type": "exec_approval_required",
......@@ -180,7 +187,7 @@ All messages are JSON over WebSocket.
### 7. Node → Gateway: Command Denied
**When command matches "deny" list:**
**When command matches the node's `permissions.deny` rules:**
```json
{
"type": "exec_denied",
......@@ -257,8 +264,14 @@ Node registers optional tools in its `tools` list during registration and expose
"type": "register",
"node_name": "sissy",
"version": "1.0",
"capabilities": ["exec", "sysinfo", "browser_control"],
"sexec_path": "/home/openclaw/.openclaw/skills/sexec/sexec.sh"
"capabilities": ["exec", "browser_control"],
"capability_info": {
"enable_browser": true,
"enable_computer_control": false,
"enable_desktop_observe": false,
"enable_audio_control": false,
"enable_camera_control": false
}
}
```
......@@ -271,13 +284,22 @@ Node registers optional tools in its `tools` list during registration and expose
**Node config file** (`/etc/hermes-node/config.json`):
```json
{
"gateway_url": "ws://192.168.42.115:8765",
"gateway_url": "wss://192.168.42.115:8765",
"node_name": "sissy",
"token": "node-sissy-secret-token-abc123",
"sexec_path": "/home/openclaw/.openclaw/skills/sexec/sexec.sh",
"reconnect_interval": 5,
"heartbeat_interval": 30,
"enable_camera_control": false
"capabilities": ["exec"],
"enable_browser": false,
"enable_computer_control": false,
"enable_desktop_observe": false,
"enable_audio_control": false,
"enable_camera_control": false,
"permissions": {
"deny": ["rm -rf /", "mkfs", "dd if="],
"ask": ["sudo", "su", "passwd", "mount", "umount", "fdisk", "parted"],
"allow": ["ls", "pwd", "whoami", "hostname", "uname", "date", "uptime", "df", "free", "ps", "cat", "head", "tail", "grep", "find", "du", "ip", "ifconfig", "ping", "curl", "wget", "git", "python", "python3", "node", "npm", "dmesg", "docker", "kubectl"]
}
}
```
......@@ -351,7 +373,7 @@ For Hermes skill to submit commands.
"last_seen": 1714392000,
"uptime": 86400,
"version": "1.0",
"capabilities": ["exec", "sysinfo"]
"capabilities": ["exec"]
}
```
......@@ -369,21 +391,21 @@ For Hermes skill to submit commands.
- Tokens stored in `/etc/hermes-node/config.json` on node
- Tokens stored in gateway registry (file or DB)
### 3. Permission System (Reused from sexec)
- Each node keeps existing `sexec.sh` + `config.json`
- `allow` list: auto-execute
- `ask` list: require user approval
- `deny` list: reject immediately
### 3. Permission System
- Each node stores its permission rules in `config.json`
- `permissions.allow`: auto-execute
- `permissions.ask`: require user approval
- `permissions.deny`: reject immediately
### 4. Command Approval Flow
```
User → Hermes → Gateway → Node
sexec checks config.json
agent checks config.json permissions
matches "ask" list
matches `permissions.ask`
sends approval_required
sends approval_required
Gateway → User: "Approve 'sudo gnt-instance stop prod-db'?"
......@@ -391,7 +413,7 @@ User: "yes"
Gateway → Node: approved=true
sexec executes
agent executes command
```
---
......@@ -427,7 +449,7 @@ Gateway → Node: approved=true
### Node Side
1. Install `hermes-node-agent` package
2. Configure `/etc/hermes-node/config.json` with gateway URL + token
3. Ensure `sexec.sh` is installed and configured
3. Review the `permissions` block and capability toggles for the node
4. Start service: `/etc/init.d/hermes-node-agent start`
5. Verify connection: `tail -f /var/log/hermes-node-agent.log`
......@@ -435,18 +457,18 @@ Gateway → Node: approved=true
## Compatibility
### With OpenClaw sexec
-Reuses exact same `sexec.sh` binary
-Reuses exact same `config.json` format
-Reuses exact same permission logic
-No changes needed to existing sexec installations
### With node-local permission policies
-Keeps the permission policy in `config.json`
-Preserves allow/deny/ask semantics
-Moves execution and approval checks fully into the agent
-Removes external wrapper dependencies from deployment
### Migration from OpenClaw
1. Keep existing sexec on nodes
2. Install node agent alongside
### Migration from earlier wrapper-based setups
1. Move any legacy allow/deny/ask rules into the node's `permissions` block
2. Install or upgrade the node agent
3. Configure gateway URL + token
4. Start agent
5. Disable OpenClaw gateway (optional)
5. Remove obsolete wrapper references from local docs/scripts
---
......
......@@ -16,7 +16,7 @@ Cross-platform node agent for the Hermes Node Protocol. Connects to a central ga
- **Cross-platform**: Linux and Windows support
- **Reverse connection**: Nodes connect to gateway (firewall-friendly)
- **Token authentication**: Secure per-node tokens
- **Permission system**: sexec-based allow/deny/ask rules
- **Permission system**: integrated allow/deny/ask rules enforced by the agent
- **Auto-reconnect**: Exponential backoff on disconnect
- **Heartbeat**: Keep-alive mechanism
- **Optional capabilities**: Browser control, computer control
......@@ -72,9 +72,19 @@ sudo /etc/init.d/hermes-node-agent start
"gateway_url": "wss://gateway-host:8765",
"node_name": "my-node",
"token": "your-token-here",
"sexec_path": "/path/to/sexec.sh",
"reconnect_interval": 5,
"heartbeat_interval": 30
"heartbeat_interval": 30,
"capabilities": ["exec"],
"enable_browser": false,
"enable_computer_control": false,
"enable_desktop_observe": false,
"enable_audio_control": false,
"enable_camera_control": false,
"permissions": {
"deny": ["rm -rf /", "mkfs", "dd if="],
"ask": ["sudo", "su", "passwd", "mount", "umount", "fdisk", "parted"],
"allow": ["ls", "pwd", "whoami", "hostname", "uname", "date", "uptime", "df", "free", "ps", "cat", "head", "tail", "grep", "find", "du", "ip", "ifconfig", "ping", "curl", "wget", "git", "python", "python3", "node", "npm", "dmesg", "docker", "kubectl"]
}
}
```
......@@ -99,7 +109,7 @@ node-agent/
├── install.sh # Linux installer
├── install-windows.ps1 # Windows PowerShell installer (legacy)
├── hermes-node-agent.init.d # SysV init script
├── hermes-node-agent.service # systemd unit (alternative)
├── hermes-node-agent.service # service file (legacy artifact; SysV init is canonical)
├── requirements.txt # Python dependencies
└── windows/ # Windows-specific components
├── agent-manager.py # System tray GUI
......
......@@ -18,6 +18,7 @@ Implements the interface expected by hermes_node_agent.py.
import asyncio
import base64
import inspect
import logging
from typing import Dict, Any, Optional
from pathlib import Path
......@@ -401,6 +402,63 @@ class BrowserController:
cdp = await page.context.new_cdp_session(page)
result = await cdp.send(method, params or {})
return {"success": True, "result": result}
async def execute(self, command: str, params: Dict[str, Any] = None) -> Dict[str, Any]:
"""Dispatch a browser_control command from the node-agent wire protocol."""
params = params or {}
layer = params.get("layer", "high_level")
page_id = params.get("page_id")
if layer == "playwright":
return await self.playwright_command(
page_id=page_id,
command=command,
args=params.get("args") or [],
kwargs=params.get("kwargs") or {},
)
if layer == "cdp":
return await self.cdp_command(
page_id=page_id,
method=command,
params=params.get("params") or {},
)
high_level_map = {
"launch": self.launch,
"create_context": self.create_context,
"new_page": self.new_page,
"navigate": self.navigate,
"click": self.click,
"fill": self.fill,
"type_text": self.type_text,
"wait_for_selector": self.wait_for_selector,
"execute_script": self.execute_script,
"evaluate": self.evaluate,
"screenshot": self.screenshot,
"get_content": self.get_content,
"get_title": self.get_title,
"get_url": self.get_url,
"close_page": self.close_page,
"close_context": self.close_context,
"close": self.close,
"list_pages": self.list_pages,
"list_contexts": self.list_contexts,
"load_extension": self.load_extension,
"execute_extension_script": self.execute_extension_script,
"list_extensions": self.list_extensions,
}
handler = high_level_map.get(command)
if handler is None:
return {"success": False, "error": f"Unknown browser command: {command}"}
call_params = dict(params)
call_params.pop("layer", None)
signature = inspect.signature(handler)
accepted = set(signature.parameters.keys())
filtered = {k: v for k, v in call_params.items() if k in accepted}
return await handler(**filtered)
# Helpers
......
This diff is collapsed.
......@@ -33,6 +33,7 @@ import ssl
import subprocess
import sys
import time
from contextlib import suppress
from pathlib import Path
from typing import Optional, Dict, Any, List
......@@ -1127,6 +1128,7 @@ class NodeAgent:
except Exception as e:
logger.warning(f"BrowserController init failed: {e}")
self.capabilities = self._detect_capabilities()
self._pending_tasks: set[asyncio.Task] = set()
def _load_config(self, path: Optional[str]) -> Dict[str, Any]:
"""Load node configuration from JSON."""
......@@ -1227,7 +1229,9 @@ class NodeAgent:
try:
async for raw in ws:
msg = json.loads(raw)
await self._handle_message(ws, msg)
task = asyncio.create_task(self._handle_message(ws, msg))
self._pending_tasks.add(task)
task.add_done_callback(self._pending_tasks.discard)
except Exception as e:
disconnect_reason = e
raise
......@@ -1237,6 +1241,13 @@ class NodeAgent:
await heartbeat_task
except asyncio.CancelledError:
pass
pending = list(self._pending_tasks)
for task in pending:
task.cancel()
for task in pending:
with suppress(asyncio.CancelledError):
await task
self._pending_tasks.clear()
_log_disconnected(disconnect_reason)
except Exception as e:
......@@ -1433,9 +1444,11 @@ class NodeAgent:
_log_browser_received(command)
try:
if hasattr(self.browser, 'execute'):
result = self.browser.execute(command, params)
result = await self.browser.execute(command, params)
elif hasattr(self.browser, 'run'):
result = self.browser.run(command, params)
if asyncio.iscoroutine(result) or isinstance(result, asyncio.Future):
result = await result
else:
result = {'success': False, 'error': 'BrowserController has no execute/run entrypoint'}
except Exception as e:
......
......@@ -96,13 +96,23 @@ if (-not (Test-Path $configPath)) {
$token = ($tokenBytes | ForEach-Object { $_.ToString("x2") }) -join ''
$config = @{
gateway_url = "wss://YOUR-GATEWAY-HOST:8765"
node_name = $env:COMPUTERNAME
token = $token
sexec_path = "$env:USERPROFILE\.openclaw\skills\sexec\sexec.ps1"
reconnect_interval = 5
heartbeat_interval = 30
} | ConvertTo-Json -Depth 3
gateway_url = "wss://YOUR-GATEWAY-HOST:8765"
node_name = $env:COMPUTERNAME
token = $token
reconnect_interval = 5
heartbeat_interval = 30
capabilities = @("exec")
enable_browser = $false
enable_computer_control = $false
enable_desktop_observe = $false
enable_audio_control = $false
enable_camera_control = $false
permissions = @{
deny = @("rm -rf /", "mkfs", "dd if=")
ask = @("sudo", "su", "passwd", "mount", "umount", "fdisk", "parted")
allow = @("ls", "pwd", "whoami", "hostname", "uname", "date", "uptime", "df", "free", "ps", "cat", "head", "tail", "grep", "find", "du", "ip", "ifconfig", "ping", "curl", "wget", "git", "python", "python3", "node", "npm", "systemctl", "journalctl", "dmesg", "docker", "kubectl")
}
} | ConvertTo-Json -Depth 5
$config | Out-File -FilePath $configPath -Encoding UTF8
Write-Host " Config written to: $configPath" -ForegroundColor Yellow
......@@ -165,5 +175,5 @@ Write-Host " Get-Content $configDir\hermes-node-agent.log -Wait -Tail 50" -For
Write-Host ""
Write-Host "=== Important ===" -ForegroundColor Yellow
Write-Host "The agent runs as SYSTEM if using Task Scheduler, or as LocalSystem if using NSSM." -ForegroundColor Gray
Write-Host "Ensure the sexec.ps1 script exists at the configured path if using permissions." -ForegroundColor Gray
Write-Host "Review the permissions block in config.json before enabling broader command access." -ForegroundColor Gray
Write-Host ""
......@@ -70,13 +70,22 @@ This is a fully self-contained offline Windows installer.
The installer writes to `C:\ProgramData\HermesNode\config.json`:
```json
{
"gateway_url": "ws://gateway.example.com:8765",
"gateway_url": "wss://gateway.example.com:8765",
"node_name": "WORKSTATION-01",
"token": "YOUR-SECRET-TOKEN",
"sexec_path": ".\sexec-template.ps1",
"reconnect_interval": 5,
"heartbeat_interval": 30,
"capabilities": ["exec", "browser_control", "computer_control"]
"capabilities": ["exec"],
"enable_browser": false,
"enable_computer_control": false,
"enable_desktop_observe": false,
"enable_audio_control": false,
"enable_camera_control": false,
"permissions": {
"deny": ["rm -rf /", "mkfs", "dd if="],
"ask": ["sudo", "su", "passwd", "mount", "umount", "fdisk", "parted"],
"allow": ["ls", "pwd", "whoami", "hostname", "uname", "date", "uptime", "df", "free", "ps", "cat", "head", "tail", "grep", "find", "du", "ip", "ifconfig", "ping", "curl", "wget", "git", "python", "python3", "node", "npm", "systemctl", "journalctl", "dmesg", "docker", "kubectl"]
}
}
```
......
......@@ -195,10 +195,19 @@ Section "Install"
''gateway_url'': ''$0'',
''node_name'': ''$1'',
''token'': ''$2'',
''sexec_path'': ''.\sexec-template.ps1'',
''reconnect_interval'': 5,
''heartbeat_interval'': 30,
''capabilities'': [''exec'', ''browser_control'', ''computer_control'']
''capabilities'': [''exec''],
''enable_browser'': $false,
''enable_computer_control'': $false,
''enable_desktop_observe'': $false,
''enable_audio_control'': $false,
''enable_camera_control'': $false,
''permissions'': @{
''deny'': @(''rm -rf /'', ''mkfs'', ''dd if=''),
''ask'': @(''sudo'', ''su'', ''passwd'', ''mount'', ''umount'', ''fdisk'', ''parted''),
''allow'': @(''ls'', ''pwd'', ''whoami'', ''hostname'', ''uname'', ''date'', ''uptime'', ''df'', ''free'', ''ps'', ''cat'', ''head'', ''tail'', ''grep'', ''find'', ''du'', ''ip'', ''ifconfig'', ''ping'', ''curl'', ''wget'', ''git'', ''python'', ''python3'', ''node'', ''npm'', ''systemctl'', ''journalctl'', ''dmesg'', ''docker'', ''kubectl'')
}
}}")"'
; Write uninstaller
......
......@@ -106,10 +106,15 @@ Section \"Install\"
FWrite $0 \" \"gateway_url\": \"\"$GatewayURL\"\",\"
FWrite $0 \" \"node_name\": \"\"$NodeName\"\",\"
FWrite $0 \" \"token\": \"\"$Token\"\",\"
FWrite $0 \" \"sexec_path\": \"./sexec-template.ps1\",\"
FWrite $0 \" \"reconnect_interval\": 5,\"
FWrite $0 \" \"heartbeat_interval\": 30,\"
FWrite $0 \" \"capabilities\": [\"exec\", \"browser_control\", \"computer_control\"]\"
FWrite $0 \" \"capabilities\": [\"exec\"],\"
FWrite $0 \" \"enable_browser\": false,\"
FWrite $0 \" \"enable_computer_control\": false,\"
FWrite $0 \" \"enable_desktop_observe\": false,\"
FWrite $0 \" \"enable_audio_control\": false,\"
FWrite $0 \" \"enable_camera_control\": false,\"
FWrite $0 \" \"permissions\": {\"deny\": [\"rm -rf /\", \"mkfs\", \"dd if=\"], \"ask\": [\"sudo\", \"su\", \"passwd\", \"mount\", \"umount\", \"fdisk\", \"parted\"], \"allow\": [\"ls\", \"pwd\", \"whoami\", \"hostname\", \"uname\", \"date\", \"uptime\", \"df\", \"free\", \"ps\", \"cat\", \"head\", \"tail\", \"grep\", \"find\", \"du\", \"ip\", \"ifconfig\", \"ping\", \"curl\", \"wget\", \"git\", \"python\", \"python3\", \"node\", \"npm\", \"systemctl\", \"journalctl\", \"dmesg\", \"docker\", \"kubectl\"]}\"
FWrite $0 \"}\"
FileClose $0
......
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