Commit c3992206 authored by Lisa (Hermes AI)'s avatar Lisa (Hermes AI)

refactor: remove browser extension and build artifacts from plugin repo;...

refactor: remove browser extension and build artifacts from plugin repo; plugin should only contain gateway code
parent 4b0e5683
Pipeline #312 canceled with stages
-----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>
// Popup UI controller
const statusEl = document.getElementById('status');
const nodeInfoEl = document.getElementById('node-info');
const nodeNameEl = document.getElementById('node-name');
const gatewayUrlEl = document.getElementById('gateway-url');
const gatewayInput = document.getElementById('gateway-input');
const nodeNameInput = document.getElementById('node-name-input');
const tokenInput = document.getElementById('token-input');
const saveBtn = document.getElementById('save-btn');
const disconnectBtn = document.getElementById('disconnect-btn');
// Load current status
chrome.runtime.sendMessage({ type: 'get_status' }, (response) => {
if (response) {
updateUI(response);
// Populate inputs
if (response.config) {
gatewayInput.value = response.config.gateway_url || '';
nodeNameInput.value = response.config.node_name || '';
// Don't populate token for security
}
}
});
// Update UI based on status
function updateUI(status) {
if (status.connected) {
statusEl.className = 'status connected';
statusEl.textContent = '● Connected';
nodeInfoEl.style.display = 'block';
nodeNameEl.textContent = status.node_name || '-';
gatewayUrlEl.textContent = status.gateway_url || '-';
saveBtn.style.display = 'none';
disconnectBtn.style.display = 'inline-block';
} else {
statusEl.className = 'status disconnected';
statusEl.textContent = '● Disconnected';
nodeInfoEl.style.display = 'none';
saveBtn.style.display = 'inline-block';
disconnectBtn.style.display = 'none';
}
}
// Save configuration and connect
saveBtn.addEventListener('click', () => {
const config = {
gateway_url: gatewayInput.value.trim(),
node_name: nodeNameInput.value.trim(),
token: tokenInput.value.trim()
};
// Validate
if (!config.gateway_url) {
alert('Gateway URL is required');
return;
}
if (!config.node_name) {
alert('Node name is required');
return;
}
if (!config.token) {
alert('Authentication token is required');
return;
}
// Validate URL format
if (!config.gateway_url.startsWith('ws://') && !config.gateway_url.startsWith('wss://')) {
alert('Gateway URL must start with ws:// or wss://');
return;
}
statusEl.className = 'status connecting';
statusEl.textContent = '● Connecting...';
saveBtn.disabled = true;
chrome.runtime.sendMessage({ type: 'update_config', config }, (response) => {
saveBtn.disabled = false;
if (response && response.success) {
// Wait a moment for connection
setTimeout(() => {
chrome.runtime.sendMessage({ type: 'get_status' }, updateUI);
}, 1000);
} else {
alert('Failed to save configuration');
statusEl.className = 'status disconnected';
statusEl.textContent = '● Disconnected';
}
});
});
// Disconnect
disconnectBtn.addEventListener('click', () => {
chrome.runtime.sendMessage({ type: 'disconnect' }, () => {
chrome.runtime.sendMessage({ type: 'get_status' }, updateUI);
});
});
// Poll status every 2 seconds
setInterval(() => {
chrome.runtime.sendMessage({ type: 'get_status' }, (response) => {
if (response) updateUI(response);
});
}, 2000);
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