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 ""
#!/bin/bash
#
# Hermes Node Agent — Self-Contained Installer
# Fully embedded agent code — no internet/download needed
#
set -e
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
CYAN='\033[0;36m'
NC='\033[0m'
echo -e "${CYAN}╔══════════════════════════════════════════════════════════════╗"
echo -e "${CYAN}║ HERMES NODE AGENT INSTALLER ║"
echo -e "${CYAN}╚═════════════════════════════════════════════════════════════╝"
echo ""
# 1. Prerequisites
echo -e "${CYAN}[1/6] Prerequisites...${NC}"
if [ "$EUID" -eq 0 ]; then
echo -e "${RED}ERROR: Run as normal user with sudo. Example: sudo $0${NC}"
exit 1
fi
command -v python3 &>/dev/null || { echo -e "${RED}✗ Python 3 required${NC}"; exit 1; }
echo -e " ${GREEN}✓ python3${NC}"
python3 -m pip install --user "websockets>=16.0" 2>/dev/null || {
command -v apt-get &>/dev/null && sudo apt-get install -y python3-websockets 2>/dev/null || true
}
# 2. Node selection
echo ""
echo -e "${CYAN}[2/6] Select node${NC}"
echo " Choose: sissy, zeiss, spank, ganeti1, ganeti2"
read -p " Node name: " NODE_NAME
case "$NODE_NAME" in
sissy) NODE_TOKEN="dbed0834bfc502f3017add9be902c9d321c9cd62f09732a55ee2f8b2b633622f" ;;
zeiss) NODE_TOKEN="6e07f6490f9e651c8bfdea10f66f0473fa091161d97d32650bb938d9070283e7" ;;
spank) NODE_TOKEN="ee0e5c368bb0ed144a6952e0b9171aac00a07e161c59c90d08fc9515c8b6f610" ;;
ganeti1) NODE_TOKEN="565d71e7aba1379940756f6b20c6c515a39b276d042ef1c87f1adcb405a42955" ;;
ganeti2) NODE_TOKEN="d05d909b0e0c890a075cee34d3e2036013e716dbbb757017a40d8b4c58d98436" ;;
*) echo -e "${RED}✗ Invalid node${NC}"; exit 1 ;;
esac
echo -e " ${GREEN}$NODE_NAME${NC}"
# 3. Gateway
read -p " Gateway host [lisa]: " GATEWAY_HOST
GATEWAY_HOST=${GATEWAY_HOST:-"lisa"}
read -p " Gateway port [8765]: " GATEWAY_PORT
GATEWAY_PORT=${GATEWAY_PORT:-"8765"}
SEXEC_PATH="$HOME/.openclaw/skills/sexec/sexec.sh"
read -p " sexec.sh [$SEXEC_PATH]: " SEXEC_PATH_INPUT
SEXEC_PATH=${SEXEC_PATH_INPUT:-$SEXEC_PATH}
echo ""
echo -e "${CYAN}[3/6] Confirm${NC}"
echo " Node: $NODE_NAME"
echo " Gateway: wss://$GATEWAY_HOST:$GATEWAY_PORT"
echo " Token: ${NODE_TOKEN:0:12}...${NODE_TOKEN: -8}"
read -p " OK? (yes/no): " CONFIRM
[ "$CONFIRM" = "yes" ] || { echo "Cancelled."; exit 0; }
# 4. Decode & install agent
echo ""
echo -e "${CYAN}[4/6] Installing agent...${NC}"
mkdir -p /tmp/hermes_install
echo "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoiIiIKSGVybWVzIE5vZGUgQWdlbnQgLSBSZXZlcnNlLWNvbm5lY3Rpb24gbm9kZSBleGVjdXRvcgoKQ29ubmVjdHMgdG8gSGVybWVzIEdhdGV3YXkgdmlhIFdlYlNvY2tldCBhbmQgZXhlY3V0ZXMgY29tbWFuZHMKdmlhIGxvY2FsIHNleGVjLnNoLCBwcmVzZXJ2aW5nIHRoZSBleGlzdGluZyBwZXJtaXNzaW9uIHN5c3RlbS4KU3VwcG9ydHMgVExTL1NTTCAod3NzOi8vKSB3aXRoIHNlbGYtc2lnbmVkIG9yIGN1c3RvbSBjZXJ0aWZpY2F0ZXMuCgpBdXRob3I6IExpc2EgKEhlcm1lcyBBSSkKRGF0ZTogMjAyNi0wNC0yOQpVcGRhdGVkOiAyMDI2LTA0LTMwIChXU1Mgc3VwcG9ydCkKIiIiCgppbXBvcnQgYXN5bmNpbwppbXBvcnQganNvbgppbXBvcnQgbG9nZ2luZwppbXBvcnQgb3MKaW1wb3J0IHNzbAppbXBvcnQgc3VicHJvY2VzcwppbXBvcnQgc3lzCmltcG9ydCB0aW1lCmltcG9ydCBhcmdwYXJzZQppbXBvcnQgc29ja2V0CmZyb20gcGF0aGxpYiBpbXBvcnQgUGF0aApmcm9tIHR5cGluZyBpbXBvcnQgT3B0aW9uYWwsIERpY3QsIEFueQoKdHJ5OgogICAgaW1wb3J0IHdlYnNvY2tldHMKZXhjZXB0IEltcG9ydEVycm9yOgogICAgcHJpbnQoIkVSUk9SOiB3ZWJzb2NrZXRzIGxpYnJhcnkgbm90IGZvdW5kLiBJbnN0YWxsIHdpdGg6IHBpcCBpbnN0YWxsIHdlYnNvY2tldHMiKQogICAgc3lzLmV4aXQoMSkKCnRyeToKICAgIGZyb20gYnJvd3Nlcl9jb250cm9sbGVyIGltcG9ydCBCcm93c2VyQ29udHJvbGxlcgogICAgQlJPV1NFUl9DT05UUk9MX0VOQUJMRUQgPSBUcnVlCmV4Y2VwdCBJbXBvcnRFcnJvcjoKICAgIEJST1dTRVJfQ09OVFJPTF9FTkFCTEVEID0gRmFsc2UKICAgIEJyb3dzZXJDb250cm9sbGVyID0gTm9uZQoKIyBDb25maWd1cmUgbG9nZ2luZwpsb2dnaW5nLmJhc2ljQ29uZmlnKAogICAgbGV2ZWw9bG9nZ2luZy5JTkZPLAogICAgZm9ybWF0PSclKGFzY3RpbWUpcyBbJShsZXZlbG5hbWUpc10gJShtZXNzYWdlKXMnLAogICAgaGFuZGxlcnM9WwogICAgICAgIGxvZ2dpbmcuU3RyZWFtSGFuZGxlcigpLAogICAgICAgIGxvZ2dpbmcuRmlsZUhhbmRsZXIoJy92YXIvbG9nL2hlcm1lcy1ub2RlLWFnZW50LmxvZycpCiAgICBdCikKbG9nZ2VyID0gbG9nZ2luZy5nZXRMb2dnZXIoX19uYW1lX18pCgpkZWYgZ2V0X2xvY2FsX2hvc3RuYW1lKCkgLT4gc3RyOgogICAgIiIiRGV0ZWN0IGxvY2FsIGhvc3RuYW1lLCBmYWxsaW5nIGJhY2sgdG8gJ3Vua25vd24nIiIiCiAgICB0cnk6CiAgICAgICAgcmV0dXJuIHNvY2tldC5nZXRob3N0bmFtZSgpCiAgICBleGNlcHQgRXhjZXB0aW9uOgogICAgICAgIHJldHVybiAidW5rbm93biIKCmNsYXNzIE5vZGVBZ2VudDoKICAgICIiIkhlcm1lcyBOb2RlIEFnZW50IC0gY29ubmVjdHMgdG8gZ2F0ZXdheSBhbmQgZXhlY3V0ZXMgY29tbWFuZHMiIiIKICAgIAogICAgZGVmIF9faW5pdF9fKHNlbGYsIGNvbmZpZ19wYXRoOiBzdHIgPSAiL2V0Yy9oZXJtZXMtbm9kZS9jb25maWcuanNvbiIpOgogICAgICAgIHNlbGYuY29uZmlnX3BhdGggPSBjb25maWdfcGF0aAogICAgICAgIHNlbGYuY29uZmlnID0gc2VsZi5fbG9hZF9jb25maWcoKQogICAgICAgIHNlbGYud3MgPSBOb25lCiAgICAgICAgc2VsZi5ydW5uaW5nID0gRmFsc2UKICAgICAgICBzZWxmLnJlY29ubmVjdF9kZWxheSA9IDUgICMgU3RhcnQgd2l0aCA1IHNlY29uZHMKICAgICAgICBzZWxmLm1heF9yZWNvbm5lY3RfZGVsYXkgPSA2MAogICAgICAgIHNlbGYubGFzdF9oZWFydGJlYXQgPSAwCiAgICAgICAgCiAgICAgICAgIyBEZXRlcm1pbmUgaWYgd2UncmUgdXNpbmcgc2VjdXJlIGNvbm5lY3Rpb24KICAgICAgICBzZWxmLnVzZV90bHMgPSBzZWxmLmNvbmZpZ1snZ2F0ZXdheV91cmwnXS5zdGFydHN3aXRoKCd3c3M6Ly8nKQogICAgICAgIAogICAgICAgICMgQnJvd3NlciBjb250cm9sbGVyCiAgICAgICAgaWYgQlJPV1NFUl9DT05UUk9MX0VOQUJMRUQ6CiAgICAgICAgICAgIHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyID0gQnJvd3NlckNvbnRyb2xsZXIoKQogICAgICAgICAgICBzZWxmLmJyb3dzZXJfY29udHJvbGxlcl9pbml0aWFsaXplZCA9IEZhbHNlCiAgICAgICAgZWxzZToKICAgICAgICAgICAgc2VsZi5icm93c2VyX2NvbnRyb2xsZXIgPSBOb25lCiAgICAgICAgICAgIHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyX2luaXRpYWxpemVkID0gRmFsc2UKICAgICAgICAKICAgICAgICAjIFNldHVwIFNTTCBjb250ZXh0IGlmIHVzaW5nIFdTUwogICAgICAgIHNlbGYuc3NsX2NvbnRleHQgPSBOb25lCiAgICAgICAgaWYgc2VsZi51c2VfdGxzOgogICAgICAgICAgICBzZWxmLnNzbF9jb250ZXh0ID0gc2VsZi5fY3JlYXRlX3NzbF9jb250ZXh0KCkKICAgIAogICAgZGVmIF9sb2FkX2NvbmZpZyhzZWxmKSAtPiBkaWN0OgogICAgICAgICIiIkxvYWQgY29uZmlndXJhdGlvbiBmcm9tIGZpbGUiIiIKICAgICAgICB0cnk6CiAgICAgICAgICAgIHdpdGggb3BlbihzZWxmLmNvbmZpZ19wYXRoKSBhcyBmOgogICAgICAgICAgICAgICAgY29uZmlnID0ganNvbi5sb2FkKGYpCiAgICAgICAgICAgIAogICAgICAgICAgICAjIFZhbGlkYXRlIHJlcXVpcmVkIGZpZWxkcwogICAgICAgICAgICByZXF1aXJlZCA9IFsiZ2F0ZXdheV91cmwiLCAibm9kZV9uYW1lIiwgInRva2VuIiwgInNleGVjX3BhdGgiXQogICAgICAgICAgICBmb3IgZmllbGQgaW4gcmVxdWlyZWQ6CiAgICAgICAgICAgICAgICBpZiBmaWVsZCBub3QgaW4gY29uZmlnOgogICAgICAgICAgICAgICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoZiJNaXNzaW5nIHJlcXVpcmVkIGNvbmZpZyBmaWVsZDoge2ZpZWxkfSIpCiAgICAgICAgICAgIAogICAgICAgICAgICAjIEF1dG8tZGV0ZWN0IGhvc3RuYW1lIGlmIHNldCB0byAnYXV0bycKICAgICAgICAgICAgaWYgY29uZmlnLmdldCgibm9kZV9uYW1lIikgPT0gImF1dG8iOgogICAgICAgICAgICAgICAgY29uZmlnWyJub2RlX25hbWUiXSA9IGdldF9sb2NhbF9ob3N0bmFtZSgpCiAgICAgICAgICAgICAgICBsb2dnZXIuaW5mbyhmIkF1dG8tZGV0ZWN0ZWQgaG9zdG5hbWU6IHtjb25maWdbJ25vZGVfbmFtZSddfSIpCiAgICAgICAgICAgIAogICAgICAgICAgICAjIFNldCBkZWZhdWx0cwogICAgICAgICAgICBjb25maWcuc2V0ZGVmYXVsdCgicmVjb25uZWN0X2ludGVydmFsIiwgNSkKICAgICAgICAgICAgY29uZmlnLnNldGRlZmF1bHQoImhlYXJ0YmVhdF9pbnRlcnZhbCIsIDMwKQogICAgICAgICAgICAKICAgICAgICAgICAgbG9nZ2VyLmluZm8oZiJMb2FkZWQgY29uZmlnIGZvciBub2RlICd7Y29uZmlnWydub2RlX25hbWUnXX0nIikKICAgICAgICAgICAgbG9nZ2VyLmluZm8oZiJHYXRld2F5IFVSTDoge2NvbmZpZ1snZ2F0ZXdheV91cmwnXX0iKQogICAgICAgICAgICByZXR1cm4gY29uZmlnCiAgICAgICAgICAgIAogICAgICAgIGV4Y2VwdCBGaWxlTm90Rm91bmRFcnJvcjoKICAgICAgICAgICAgbG9nZ2VyLmVycm9yKGYiQ29uZmlnIGZpbGUgbm90IGZvdW5kOiB7c2VsZi5jb25maWdfcGF0aH0iKQogICAgICAgICAgICBsb2dnZXIuaW5mbygiQ3JlYXRlIGNvbmZpZyBmaWxlIHdpdGg6IikKICAgICAgICAgICAgbG9nZ2VyLmluZm8oanNvbi5kdW1wcyh7CiAgICAgICAgICAgICAgICAiZ2F0ZXdheV91cmwiOiAid3NzOi8veW91ci1nYXRld2F5Ojg3NjUiLAogICAgICAgICAgICAgICAgIm5vZGVfbmFtZSI6ICJhdXRvIiwKICAgICAgICAgICAgICAgICJ0b2tlbiI6ICJ5b3VyLXNlY3JldC10b2tlbiIsCiAgICAgICAgICAgICAgICAic2V4ZWNfcGF0aCI6ICIvaG9tZS9vcGVuY2xhdy8ub3BlbmNsYXcvc2tpbGxzL3NleGVjL3NleGVjLnNoIiwKICAgICAgICAgICAgICAgICJnYXRld2F5X2NlcnRfcGF0aCI6ICIvZXRjL2hlcm1lcy1ub2RlL2NlcnRzL2dhdGV3YXkuY3J0IiwKICAgICAgICAgICAgICAgICJyZWNvbm5lY3RfaW50ZXJ2YWwiOiA1LAogICAgICAgICAgICAgICAgImhlYXJ0YmVhdF9pbnRlcnZhbCI6IDMwCiAgICAgICAgICAgIH0sIGluZGVudD0yKSkKICAgICAgICAgICAgc3lzLmV4aXQoMSkKICAgICAgICBleGNlcHQganNvbi5KU09ORGVjb2RlRXJyb3IgYXMgZToKICAgICAgICAgICAgbG9nZ2VyLmVycm9yKGYiSW52YWxpZCBKU09OIGluIGNvbmZpZyBmaWxlOiB7ZX0iKQogICAgICAgICAgICBzeXMuZXhpdCgxKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgbG9nZ2VyLmVycm9yKGYiRXJyb3IgbG9hZGluZyBjb25maWc6IHtlfSIpCiAgICAgICAgICAgIHN5cy5leGl0KDEpCiAgICAKICAgIGRlZiBfY3JlYXRlX3NzbF9jb250ZXh0KHNlbGYpIC0+IHNzbC5TU0xDb250ZXh0OgogICAgICAgICIiIkNyZWF0ZSBTU0wgY29udGV4dCBmb3IgV1NTIGNvbm5lY3Rpb25zIiIiCiAgICAgICAgY2VydF9wYXRoID0gc2VsZi5jb25maWcuZ2V0KCJnYXRld2F5X2NlcnRfcGF0aCIsICIvZXRjL2hlcm1lcy1ub2RlL2NlcnRzL2dhdGV3YXkuY3J0IikKICAgICAgICAKICAgICAgICBpZiBub3Qgb3MucGF0aC5leGlzdHMoY2VydF9wYXRoKToKICAgICAgICAgICAgbG9nZ2VyLmVycm9yKGYiR2F0ZXdheSBjZXJ0aWZpY2F0ZSBub3QgZm91bmQ6IHtjZXJ0X3BhdGh9IikKICAgICAgICAgICAgbG9nZ2VyLmVycm9yKCJDb3B5IHRoZSBnYXRld2F5J3Mgc2VsZi1zaWduZWQgY2VydGlmaWNhdGUgdG8gdGhhdCBwYXRoIikKICAgICAgICAgICAgbG9nZ2VyLmVycm9yKCJPbiB0aGUgZ2F0ZXdheTogL2hvbWUvbGlzYS9oZXJtZXMtbm9kZS1wcm90b2NvbC9nYXRld2F5L2NlcnRzL2dhdGV3YXkuY3J0IikKICAgICAgICAgICAgcmFpc2UgRmlsZU5vdEZvdW5kRXJyb3IoZiJDZXJ0aWZpY2F0ZSBub3QgZm91bmQ6IHtjZXJ0X3BhdGh9IikKICAgICAgICAKICAgICAgICAjIENoZWNrIGNlcnRpZmljYXRlIGV4cGlyYXRpb24KICAgICAgICB0cnk6CiAgICAgICAgICAgIGltcG9ydCBzdWJwcm9jZXNzCiAgICAgICAgICAgIHJlc3VsdCA9IHN1YnByb2Nlc3MucnVuKAogICAgICAgICAgICAgICAgWydvcGVuc3NsJywgJ3g1MDknLCAnLWluJywgY2VydF9wYXRoLCAnLW5vb3V0JywgJy1lbmRkYXRlJ10sCiAgICAgICAgICAgICAgICBjYXB0dXJlX291dHB1dD1UcnVlLCB0ZXh0PVRydWUsIHRpbWVvdXQ9NQogICAgICAgICAgICApCiAgICAgICAgICAgIGlmIHJlc3VsdC5yZXR1cm5jb2RlID09IDA6CiAgICAgICAgICAgICAgICAjIFBhcnNlOiBub3RBZnRlcj1BcHIgMjYgMjA6Mjg6MTUgMjAzNiBHTVQKICAgICAgICAgICAgICAgIGV4cGlyeV9saW5lID0gcmVzdWx0LnN0ZG91dC5zdHJpcCgpCiAgICAgICAgICAgICAgICBleHBpcnlfZGF0ZV9zdHIgPSBleHBpcnlfbGluZS5zcGxpdCgnPScsIDEpWzFdCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICMgQ29udmVydCB0byBlcG9jaCBzZWNvbmRzCiAgICAgICAgICAgICAgICByZXN1bHQyID0gc3VicHJvY2Vzcy5ydW4oCiAgICAgICAgICAgICAgICAgICAgWydkYXRlJywgJy1kJywgZXhwaXJ5X2RhdGVfc3RyLCAnKyVzJ10sCiAgICAgICAgICAgICAgICAgICAgY2FwdHVyZV9vdXRwdXQ9VHJ1ZSwgdGV4dD1UcnVlLCB0aW1lb3V0PTUKICAgICAgICAgICAgICAgICkKICAgICAgICAgICAgICAgIGlmIHJlc3VsdDIucmV0dXJuY29kZSA9PSAwOgogICAgICAgICAgICAgICAgICAgIGV4cGlyeV9lcG9jaCA9IGludChyZXN1bHQyLnN0ZG91dC5zdHJpcCgpKQogICAgICAgICAgICAgICAgICAgIG5vd19lcG9jaCA9IGludCh0aW1lLnRpbWUoKSkKICAgICAgICAgICAgICAgICAgICBkYXlzX3JlbWFpbmluZyA9IChleHBpcnlfZXBvY2ggLSBub3dfZXBvY2gpIC8vIDg2NDAwCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgaWYgZGF5c19yZW1haW5pbmcgPCAwOgogICAgICAgICAgICAgICAgICAgICAgICBsb2dnZXIuZXJyb3IoZiLimqDvuI8gIEdBVEVXQVkgQ0VSVElGSUNBVEUgSEFTIEVYUElSRUQge2FicyhkYXlzX3JlbWFpbmluZyl9IGRheXMgYWdvISIpCiAgICAgICAgICAgICAgICAgICAgICAgIGxvZ2dlci5lcnJvcihmIiAgIEV4cGlyeToge2V4cGlyeV9kYXRlX3N0cn0iKQogICAgICAgICAgICAgICAgICAgICAgICBsb2dnZXIuZXJyb3IoZiIgICBSZWdlbmVyYXRlIG9uIHRoZSBnYXRld2F5IHdpdGg6IikKICAgICAgICAgICAgICAgICAgICAgICAgbG9nZ2VyLmVycm9yKGYiICAgICBjZCAvaG9tZS9saXNhL2hlcm1lcy1ub2RlLXByb3RvY29sL2dhdGV3YXkvY2VydHMiKQogICAgICAgICAgICAgICAgICAgICAgICBsb2dnZXIuZXJyb3IoZiIgICAgIG9wZW5zc2wgcmVxIC14NTA5IC1uZXdrZXkgcnNhOjQwOTYgLWtleW91dCBnYXRld2F5LmtleSAtb3V0IGdhdGV3YXkuY3J0IC1kYXlzIDM2NTAgLW5vZGVzIikKICAgICAgICAgICAgICAgICAgICAgICAgIyBEb24ndCByYWlzZSB5ZXQg4oCUIGFsbG93IGNvbm5lY3Rpb24gYXR0ZW1wdCB0byBmYWlsIHdpdGggU1NMIGVycm9yCiAgICAgICAgICAgICAgICAgICAgZWxpZiBkYXlzX3JlbWFpbmluZyA8IDMwOgogICAgICAgICAgICAgICAgICAgICAgICBsb2dnZXIud2FybmluZyhmIuKaoO+4jyAgR2F0ZXdheSBjZXJ0aWZpY2F0ZSBleHBpcmVzIGluIHtkYXlzX3JlbWFpbmluZ30gZGF5czoge2V4cGlyeV9kYXRlX3N0cn0iKQogICAgICAgICAgICAgICAgICAgICAgICBsb2dnZXIud2FybmluZyhmIiAgIFJlZ2VuZXJhdGUgc29vbiBvbiB0aGUgZ2F0ZXdheSB0byBhdm9pZCBzZXJ2aWNlIGludGVycnVwdGlvbi4iKQogICAgICAgICAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICAgICAgICAgIGxvZ2dlci5pbmZvKGYi4pyFIEdhdGV3YXkgY2VydGlmaWNhdGUgdmFsaWQgZm9yIHtkYXlzX3JlbWFpbmluZ30gbW9yZSBkYXlzIChleHBpcmVzOiB7ZXhwaXJ5X2RhdGVfc3RyfSkiKQogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgbG9nZ2VyLndhcm5pbmcoZiJDb3VsZCBub3QgY2hlY2sgY2VydGlmaWNhdGUgZXhwaXJ5OiB7ZX0iKQogICAgICAgIAogICAgICAgICMgQ3JlYXRlIFNTTCBjb250ZXh0IHRoYXQgdmVyaWZpZXMgdGhlIHNlcnZlcidzIGNlcnQKICAgICAgICBjb250ZXh0ID0gc3NsLmNyZWF0ZV9kZWZhdWx0X2NvbnRleHQoY2FmaWxlPWNlcnRfcGF0aCkKICAgICAgICAjIEZvciBzZWxmLXNpZ25lZCwgd2UgZXhwbGljaXRseSB0cnVzdCB0aGlzIG9uZSBjZXJ0CiAgICAgICAgY29udGV4dC5jaGVja19ob3N0bmFtZSA9IEZhbHNlICAjIFNlbGYtc2lnbmVkIGNlcnRzIHVzdWFsbHkgZG9uJ3QgbWF0Y2ggaG9zdG5hbWUKICAgICAgICAKICAgICAgICBsb2dnZXIuaW5mbyhmIlNTTCBjb250ZXh0IGNvbmZpZ3VyZWQgd2l0aCBjZXJ0OiB7Y2VydF9wYXRofSIpCiAgICAgICAgcmV0dXJuIGNvbnRleHQKICAgIAogICAgYXN5bmMgZGVmIGNvbm5lY3Qoc2VsZik6CiAgICAgICAgIiIiQ29ubmVjdCB0byBnYXRld2F5IHdpdGggYXV0aGVudGljYXRpb24iIiIKICAgICAgICB1cmwgPSBmIntzZWxmLmNvbmZpZ1snZ2F0ZXdheV91cmwnXX0vbm9kZXM/dG9rZW49e3NlbGYuY29uZmlnWyd0b2tlbiddfSIKICAgICAgICAKICAgICAgICB0cnk6CiAgICAgICAgICAgIGxvZ2dlci5pbmZvKGYiQ29ubmVjdGluZyB0byBnYXRld2F5OiB7c2VsZi5jb25maWdbJ2dhdGV3YXlfdXJsJ119IikKICAgICAgICAgICAgCiAgICAgICAgICAgICMgQ29ubmVjdCB3aXRoIFNTTCBjb250ZXh0IGlmIHVzaW5nIFRMUwogICAgICAgICAgICBjb25uZWN0X2t3YXJncyA9IHsKICAgICAgICAgICAgICAgICdwaW5nX2ludGVydmFsJzogMjAsCiAgICAgICAgICAgICAgICAncGluZ190aW1lb3V0JzogMTAsCiAgICAgICAgICAgICAgICAnc3NsJzogc2VsZi5zc2xfY29udGV4dCBpZiBzZWxmLnVzZV90bHMgZWxzZSBOb25lCiAgICAgICAgICAgIH0KICAgICAgICAgICAgCiAgICAgICAgICAgIHNlbGYud3MgPSBhd2FpdCB3ZWJzb2NrZXRzLmNvbm5lY3QodXJsLCAqKmNvbm5lY3Rfa3dhcmdzKQogICAgICAgICAgICBsb2dnZXIuaW5mbygiQ29ubmVjdGVkIHRvIGdhdGV3YXkiKQogICAgICAgICAgICAKICAgICAgICAgICAgIyBTZW5kIHJlZ2lzdHJhdGlvbgogICAgICAgICAgICBhd2FpdCBzZWxmLl9yZWdpc3RlcigpCiAgICAgICAgICAgIAogICAgICAgICAgICAjIFJlc2V0IHJlY29ubmVjdCBkZWxheSBvbiBzdWNjZXNzZnVsIGNvbm5lY3Rpb24KICAgICAgICAgICAgc2VsZi5yZWNvbm5lY3RfZGVsYXkgPSBzZWxmLmNvbmZpZ1sicmVjb25uZWN0X2ludGVydmFsIl0KICAgICAgICAgICAgCiAgICAgICAgICAgIHJldHVybiBUcnVlCiAgICAgICAgICAgIAogICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgbG9nZ2VyLmVycm9yKGYiQ29ubmVjdGlvbiBmYWlsZWQ6IHtlfSIpCiAgICAgICAgICAgIHJldHVybiBGYWxzZQogICAgCiAgICBhc3luYyBkZWYgX3JlZ2lzdGVyKHNlbGYpOgogICAgICAgICIiIlNlbmQgcmVnaXN0cmF0aW9uIG1lc3NhZ2UgdG8gZ2F0ZXdheSIiIgogICAgICAgIGNhcGFiaWxpdGllcyA9IFsiZXhlYyIsICJzeXNpbmZvIl0KICAgICAgICBpZiBCUk9XU0VSX0NPTlRST0xfRU5BQkxFRDoKICAgICAgICAgICAgY2FwYWJpbGl0aWVzLmFwcGVuZCgiYnJvd3Nlcl9jb250cm9sIikKICAgICAgICAKICAgICAgICByZWdpc3RyYXRpb24gPSB7CiAgICAgICAgICAgICJ0eXBlIjogInJlZ2lzdGVyIiwKICAgICAgICAgICAgIm5vZGVfbmFtZSI6IHNlbGYuY29uZmlnWyJub2RlX25hbWUiXSwKICAgICAgICAgICAgInZlcnNpb24iOiAiMi4wIiwgICMgVXBkYXRlZCB2ZXJzaW9uIHdpdGggV1NTIHN1cHBvcnQKICAgICAgICAgICAgImNhcGFiaWxpdGllcyI6IGNhcGFiaWxpdGllcywKICAgICAgICAgICAgInNleGVjX3BhdGgiOiBzZWxmLmNvbmZpZ1sic2V4ZWNfcGF0aCJdCiAgICAgICAgfQogICAgICAgIAogICAgICAgIGF3YWl0IHNlbGYud3Muc2VuZChqc29uLmR1bXBzKHJlZ2lzdHJhdGlvbikpCiAgICAgICAgbG9nZ2VyLmluZm8oZiJTZW50IHJlZ2lzdHJhdGlvbiBmb3Igbm9kZSAne3NlbGYuY29uZmlnWydub2RlX25hbWUnXX0nIikKICAgICAgICAKICAgICAgICAjIFdhaXQgZm9yIGFjawogICAgICAgIHJlc3BvbnNlID0gYXdhaXQgc2VsZi53cy5yZWN2KCkKICAgICAgICBtc2cgPSBqc29uLmxvYWRzKHJlc3BvbnNlKQogICAgICAgIAogICAgICAgIGlmIG1zZy5nZXQoInR5cGUiKSA9PSAicmVnaXN0ZXJfYWNrIiBhbmQgbXNnLmdldCgic3RhdHVzIikgPT0gIm9rIjoKICAgICAgICAgICAgbG9nZ2VyLmluZm8oZiJSZWdpc3RyYXRpb24gYWNrbm93bGVkZ2VkIGJ5IGdhdGV3YXkgKHZlcnNpb24ge21zZy5nZXQoJ2dhdGV3YXlfdmVyc2lvbicpfSkiKQogICAgICAgIGVsc2U6CiAgICAgICAgICAgIGxvZ2dlci53YXJuaW5nKGYiVW5leHBlY3RlZCByZWdpc3RyYXRpb24gcmVzcG9uc2U6IHttc2d9IikKICAgIAogICAgYXN5bmMgZGVmIF9zZW5kX2hlYXJ0YmVhdChzZWxmKToKICAgICAgICAiIiJTZW5kIHBlcmlvZGljIGhlYXJ0YmVhdCB0byBnYXRld2F5IiIiCiAgICAgICAgd2hpbGUgc2VsZi5ydW5uaW5nIGFuZCBzZWxmLndzOgogICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICBhd2FpdCBhc3luY2lvLnNsZWVwKHNlbGYuY29uZmlnWyJoZWFydGJlYXRfaW50ZXJ2YWwiXSkKICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgaWYgbm90IHNlbGYud3Mgb3Igc2VsZi53cy5zdGF0ZS5uYW1lID09ICJDTE9TRUQiOgogICAgICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIGhlYXJ0YmVhdCA9IHsKICAgICAgICAgICAgICAgICAgICAidHlwZSI6ICJoZWFydGJlYXQiLAogICAgICAgICAgICAgICAgICAgICJ0aW1lc3RhbXAiOiBpbnQodGltZS50aW1lKCkpCiAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIGF3YWl0IHNlbGYud3Muc2VuZChqc29uLmR1bXBzKGhlYXJ0YmVhdCkpCiAgICAgICAgICAgICAgICBzZWxmLmxhc3RfaGVhcnRiZWF0ID0gdGltZS50aW1lKCkKICAgICAgICAgICAgICAgIGxvZ2dlci5kZWJ1ZygiSGVhcnRiZWF0IHNlbnQiKQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICAgICAgICAgIGxvZ2dlci5lcnJvcihmIkhlYXJ0YmVhdCBlcnJvcjoge2V9IikKICAgICAgICAgICAgICAgIGJyZWFrCiAgICAKICAgIGFzeW5jIGRlZiBfaGFuZGxlX21lc3NhZ2Uoc2VsZiwgbWVzc2FnZTogc3RyKToKICAgICAgICAiIiJIYW5kbGUgaW5jb21pbmcgbWVzc2FnZSBmcm9tIGdhdGV3YXkiIiIKICAgICAgICB0cnk6CiAgICAgICAgICAgIG1zZyA9IGpzb24ubG9hZHMobWVzc2FnZSkKICAgICAgICAgICAgbXNnX3R5cGUgPSBtc2cuZ2V0KCJ0eXBlIikKICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIG1zZ190eXBlID09ICJoZWFydGJlYXRfYWNrIjoKICAgICAgICAgICAgICAgIGxvZ2dlci5kZWJ1ZygiSGVhcnRiZWF0IGFja25vd2xlZGdlZCIpCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgZWxpZiBtc2dfdHlwZSA9PSAiZXhlYyI6CiAgICAgICAgICAgICAgICBhd2FpdCBzZWxmLl9oYW5kbGVfZXhlYyhtc2cpCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgZWxpZiBtc2dfdHlwZSA9PSAiZXhlY19jYW5jZWwiOgogICAgICAgICAgICAgICAgYXdhaXQgc2VsZi5faGFuZGxlX2NhbmNlbChtc2cpCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgZWxpZiBtc2dfdHlwZSA9PSAiYnJvd3Nlcl9jb250cm9sIjoKICAgICAgICAgICAgICAgIGF3YWl0IHNlbGYuX2hhbmRsZV9icm93c2VyX2NvbnRyb2wobXNnKQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgIGVsaWYgbXNnX3R5cGUgPT0gImRpc2Nvbm5lY3QiOgogICAgICAgICAgICAgICAgbG9nZ2VyLmluZm8oZiJHYXRld2F5IHJlcXVlc3RlZCBkaXNjb25uZWN0OiB7bXNnLmdldCgncmVhc29uJyl9IikKICAgICAgICAgICAgICAgIHNlbGYucnVubmluZyA9IEZhbHNlCiAgICAgICAgICAgICAgICAKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIGxvZ2dlci53YXJuaW5nKGYiVW5rbm93biBtZXNzYWdlIHR5cGU6IHttc2dfdHlwZX0iKQogICAgICAgICAgICAgICAgCiAgICAgICAgZXhjZXB0IGpzb24uSlNPTkRlY29kZUVycm9yIGFzIGU6CiAgICAgICAgICAgIGxvZ2dlci5lcnJvcihmIkludmFsaWQgSlNPTiByZWNlaXZlZDoge2V9IikKICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgIGxvZ2dlci5lcnJvcihmIkVycm9yIGhhbmRsaW5nIG1lc3NhZ2U6IHtlfSIpCiAgICAKICAgIGFzeW5jIGRlZiBfaGFuZGxlX2Jyb3dzZXJfY29udHJvbChzZWxmLCBtc2c6IERpY3Rbc3RyLCBBbnldKToKICAgICAgICAiIiIKICAgICAgICBIYW5kbGUgYnJvd3NlciBjb250cm9sIGNvbW1hbmRzIHdpdGggMy1sYXllciBpbnRlcmZhY2U6CiAgICAgICAgCiAgICAgICAgTGF5ZXIgMSAtIEhJR0ggTEVWRUw6IG5hdmlnYXRlLCBjbGljaywgZmlsbCwgc2NyZWVuc2hvdCwgZXhlY3V0ZV9zY3JpcHQsIGNsb3NlCiAgICAgICAgTGF5ZXIgMiAtIFBMQVlXUklHSFQ6IERpcmVjdCBQbGF5d3JpZ2h0IEFQSSBhY2Nlc3MKICAgICAgICBMYXllciAzIC0gQ0RQOiBDaHJvbWUgRGV2VG9vbHMgUHJvdG9jb2wgYWNjZXNzCiAgICAgICAgIiIiCiAgICAgICAgaWYgbm90IHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyOgogICAgICAgICAgICBhd2FpdCBzZWxmLl9zZW5kX3Jlc3BvbnNlKG1zZy5nZXQoImlkIiksICJlcnJvciIsIAogICAgICAgICAgICAgICAgZXJyb3I9IlBsYXl3cmlnaHQgbm90IGluc3RhbGxlZC4gSW5zdGFsbCB3aXRoOiBwaXAgaW5zdGFsbCBwbGF5d3JpZ2h0ICYmIHBsYXl3cmlnaHQgaW5zdGFsbCBjaHJvbWl1bSIpCiAgICAgICAgICAgIHJldHVybgogICAgICAgIAogICAgICAgICMgSW5pdGlhbGl6ZSBQbGF5d3JpZ2h0IG9uIGZpcnN0IHVzZQogICAgICAgIGlmIG5vdCBzZWxmLmJyb3dzZXJfY29udHJvbGxlcl9pbml0aWFsaXplZDoKICAgICAgICAgICAgdHJ5OgogICAgICAgICAgICAgICAgYXdhaXQgc2VsZi5icm93c2VyX2NvbnRyb2xsZXIuaW5pdGlhbGl6ZSgpCiAgICAgICAgICAgICAgICBzZWxmLmJyb3dzZXJfY29udHJvbGxlcl9pbml0aWFsaXplZCA9IFRydWUKICAgICAgICAgICAgICAgIGxvZ2dlci5pbmZvKCJCcm93c2VyIGNvbnRyb2xsZXIgaW5pdGlhbGl6ZWQiKQogICAgICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgICAgICBhd2FpdCBzZWxmLl9zZW5kX3Jlc3BvbnNlKG1zZy5nZXQoImlkIiksICJlcnJvciIsCiAgICAgICAgICAgICAgICAgICAgZXJyb3I9ZiJCcm93c2VyIGNvbnRyb2xsZXIgaW5pdCBmYWlsZWQ6IHtlfSIpCiAgICAgICAgICAgICAgICByZXR1cm4KICAgICAgICAKICAgICAgICBjbWRfaWQgPSBtc2cuZ2V0KCJpZCIpCiAgICAgICAgY29tbWFuZCA9IG1zZy5nZXQoImNvbW1hbmQiKQogICAgICAgIGxheWVyID0gbXNnLmdldCgibGF5ZXIiLCAiaGlnaF9sZXZlbCIpICAjIGhpZ2hfbGV2ZWwsIHBsYXl3cmlnaHQsIGNkcAogICAgICAgIAogICAgICAgIGxvZ2dlci5pbmZvKGYiQnJvd3NlciBjb250cm9sIGNvbW1hbmQ6IHtsYXllcn0ve2NvbW1hbmR9IGZvciBjbWQge2NtZF9pZH0iKQogICAgICAgIAogICAgICAgIHRyeToKICAgICAgICAgICAgaWYgbGF5ZXIgPT0gImhpZ2hfbGV2ZWwiOgogICAgICAgICAgICAgICAgcmVzdWx0ID0gYXdhaXQgc2VsZi5faGFuZGxlX2hpZ2hfbGV2ZWxfY29tbWFuZChtc2cpCiAgICAgICAgICAgIGVsaWYgbGF5ZXIgPT0gInBsYXl3cmlnaHQiOgogICAgICAgICAgICAgICAgcmVzdWx0ID0gYXdhaXQgc2VsZi5faGFuZGxlX3BsYXl3cmlnaHRfY29tbWFuZChtc2cpCiAgICAgICAgICAgIGVsaWYgbGF5ZXIgPT0gImNkcCI6CiAgICAgICAgICAgICAgICByZXN1bHQgPSBhd2FpdCBzZWxmLl9oYW5kbGVfY2RwX2NvbW1hbmQobXNnKQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgcmVzdWx0ID0geyJzdWNjZXNzIjogRmFsc2UsICJlcnJvciI6IGYiVW5rbm93biBsYXllcjoge2xheWVyfSJ9CiAgICAgICAgICAgIAogICAgICAgICAgICByZXN1bHRfdHlwZSA9ICJvayIgaWYgcmVzdWx0LmdldCgic3VjY2VzcyIpIGVsc2UgImVycm9yIgogICAgICAgICAgICByZXN1bHQucG9wKCJzdWNjZXNzIiwgTm9uZSkKICAgICAgICAgICAgYXdhaXQgc2VsZi5fc2VuZF9yZXNwb25zZShjbWRfaWQsIHJlc3VsdF90eXBlLCAqKnJlc3VsdCkKICAgICAgICAgICAgCiAgICAgICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBlOgogICAgICAgICAgICBsb2dnZXIuZXJyb3IoZiJCcm93c2VyIGNvbnRyb2wgZXJyb3I6IHtlfSIsIGV4Y19pbmZvPVRydWUpCiAgICAgICAgICAgIGF3YWl0IHNlbGYuX3NlbmRfcmVzcG9uc2UoY21kX2lkLCAiZXJyb3IiLCBlcnJvcj1zdHIoZSkpCiAgICAKICAgIGFzeW5jIGRlZiBfaGFuZGxlX2hpZ2hfbGV2ZWxfY29tbWFuZChzZWxmLCBtc2c6IERpY3Rbc3RyLCBBbnldKSAtPiBEaWN0W3N0ciwgQW55XToKICAgICAgICAiIiJIYW5kbGUgaGlnaC1sZXZlbCBicm93c2VyIGNvbW1hbmRzIiIiCiAgICAgICAgY29tbWFuZCA9IG1zZy5nZXQoImNvbW1hbmQiKQogICAgICAgIHBhcmFtcyA9IG1zZy5nZXQoInBhcmFtcyIsIHt9KQogICAgICAgIHBhZ2VfaWQgPSBtc2cuZ2V0KCJwYWdlX2lkIikKICAgICAgICAKICAgICAgICBjbWRfbWFwID0gewogICAgICAgICAgICAibGF1bmNoIjogKHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyLmxhdW5jaCwgW3BhcmFtcy5nZXQoImNvbmZpZyIsIHt9KV0sIHt9KSwKICAgICAgICAgICAgImNyZWF0ZV9jb250ZXh0IjogKHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyLmNyZWF0ZV9jb250ZXh0LCBbcGFyYW1zLmdldCgiY29uZmlnIiwge30pXSwge30pLAogICAgICAgICAgICAibmV3X3BhZ2UiOiAoc2VsZi5icm93c2VyX2NvbnRyb2xsZXIubmV3X3BhZ2UsIFtwYXJhbXMuZ2V0KCJjb250ZXh0X25hbWUiKV0sIHt9KSwKICAgICAgICAgICAgIm5hdmlnYXRlIjogKHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyLm5hdmlnYXRlLCBbcGFnZV9pZCwgcGFyYW1zLmdldCgidXJsIildLCAKICAgICAgICAgICAgICAgICAgICAgICAgeyJ3YWl0X3VudGlsIjogcGFyYW1zLmdldCgid2FpdF91bnRpbCIsICJsb2FkIil9KSwKICAgICAgICAgICAgImNsaWNrIjogKHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyLmNsaWNrLCBbcGFnZV9pZCwgcGFyYW1zLmdldCgic2VsZWN0b3IiKV0sCiAgICAgICAgICAgICAgICAgICAgIHtrOiB2IGZvciBrLCB2IGluIHBhcmFtcy5pdGVtcygpIGlmIGsgbm90IGluIFsic2VsZWN0b3IiLCAiY29tbWFuZCJdfSksCiAgICAgICAgICAgICJmaWxsIjogKHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyLmZpbGwsIFtwYWdlX2lkLCBwYXJhbXMuZ2V0KCJzZWxlY3RvciIpLCBwYXJhbXMuZ2V0KCJ2YWx1ZSIpXSwge30pLAogICAgICAgICAgICAidHlwZV90ZXh0IjogKHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyLnR5cGVfdGV4dCwgW3BhZ2VfaWQsIHBhcmFtcy5nZXQoInNlbGVjdG9yIiksIHBhcmFtcy5nZXQoInRleHQiKV0sCiAgICAgICAgICAgICAgICAgICAgICAgICB7ImRlbGF5IjogcGFyYW1zLmdldCgiZGVsYXkiLCAwKX0pLAogICAgICAgICAgICAid2FpdF9mb3Jfc2VsZWN0b3IiOiAoc2VsZi5icm93c2VyX2NvbnRyb2xsZXIud2FpdF9mb3Jfc2VsZWN0b3IsIFtwYWdlX2lkLCBwYXJhbXMuZ2V0KCJzZWxlY3RvciIpXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgeyJzdGF0ZSI6IHBhcmFtcy5nZXQoInN0YXRlIiwgInZpc2libGUiKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJ0aW1lb3V0IjogcGFyYW1zLmdldCgidGltZW91dCIsIDMwMDAwKX0pLAogICAgICAgICAgICAiZXhlY3V0ZV9zY3JpcHQiOiAoc2VsZi5icm93c2VyX2NvbnRyb2xsZXIuZXhlY3V0ZV9zY3JpcHQsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBbcGFnZV9pZCwgcGFyYW1zLmdldCgic2NyaXB0IildLCB7fSksCiAgICAgICAgICAgICJldmFsdWF0ZSI6IChzZWxmLmJyb3dzZXJfY29udHJvbGxlci5ldmFsdWF0ZSwgCiAgICAgICAgICAgICAgICAgICAgICAgIFtwYWdlX2lkLCBwYXJhbXMuZ2V0KCJleHByZXNzaW9uIildLCAKICAgICAgICAgICAgICAgICAgICAgICAgeyJhcmciOiBwYXJhbXMuZ2V0KCJhcmciKX0pLAogICAgICAgICAgICAic2NyZWVuc2hvdCI6IChzZWxmLmJyb3dzZXJfY29udHJvbGxlci5zY3JlZW5zaG90LCBbcGFnZV9pZF0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgeyJmdWxsX3BhZ2UiOiBwYXJhbXMuZ2V0KCJmdWxsX3BhZ2UiLCBGYWxzZSksCiAgICAgICAgICAgICAgICAgICAgICAgICAgICJwYXRoIjogcGFyYW1zLmdldCgicGF0aCIpfSksCiAgICAgICAgICAgICJnZXRfY29udGVudCI6IChzZWxmLmJyb3dzZXJfY29udHJvbGxlci5nZXRfY29udGVudCwgW3BhZ2VfaWRdLCB7fSksCiAgICAgICAgICAgICJnZXRfdGl0bGUiOiAoc2VsZi5icm93c2VyX2NvbnRyb2xsZXIuZ2V0X3RpdGxlLCBbcGFnZV9pZF0sIHt9KSwKICAgICAgICAgICAgImNsb3NlX3BhZ2UiOiAoc2VsZi5icm93c2VyX2NvbnRyb2xsZXIuY2xvc2VfcGFnZSwgW3BhZ2VfaWRdLCB7fSksCiAgICAgICAgICAgICJjbG9zZV9jb250ZXh0IjogKHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyLmNsb3NlX2NvbnRleHQsIFtwYXJhbXMuZ2V0KCJjb250ZXh0X25hbWUiKV0sIHt9KSwKICAgICAgICAgICAgImNsb3NlIjogKHNlbGYuYnJvd3Nlcl9jb250cm9sbGVyLmNsb3NlLCBbXSwge30pLAogICAgICAgICAgICAibGlzdF9wYWdlcyI6IChzZWxmLmJyb3dzZXJfY29udHJvbGxlci5saXN0X3BhZ2VzLCBbXSwge30pLAogICAgICAgICAgICAibGlzdF9jb250ZXh0cyI6IChzZWxmLmJyb3dzZXJfY29udHJvbGxlci5saXN0X2NvbnRleHRzLCBbXSwge30pLAogICAgICAgIH0KICAgICAgICAKICAgICAgICBoYW5kbGVyID0gY21kX21hcC5nZXQoY29tbWFuZCkKICAgICAgICBpZiBub3QgaGFuZGxlcjoKICAgICAgICAgICAgcmV0dXJuIHsic3VjY2VzcyI6IEZhbHNlLCAiZXJyb3IiOiBmIlVua25vd24gY29tbWFuZDoge2NvbW1hbmR9In0KICAgICAgICAKICAgICAgICBmdW5jLCBhcmdzLCBrd2FyZ3MgPSBoYW5kbGVyCiAgICAgICAgcmVzdWx0ID0gYXdhaXQgZnVuYygqYXJncywgKiprd2FyZ3MpCiAgICAgICAgcmV0dXJuIHJlc3VsdAogICAgCiAgICBhc3luYyBkZWYgX2hhbmRsZV9wbGF5d3JpZ2h0X2NvbW1hbmQoc2VsZiwgbXNnOiBEaWN0W3N0ciwgQW55XSkgLT4gRGljdFtzdHIsIEFueV06CiAgICAgICAgIiIiSGFuZGxlIGRpcmVjdCBQbGF5d3JpZ2h0IEFQSSBjb21tYW5kcyIiIgogICAgICAgIGNvbW1hbmQgPSBtc2cuZ2V0KCJjb21tYW5kIikKICAgICAgICBwYXJhbXMgPSBtc2cuZ2V0KCJwYXJhbXMiLCB7fSkKICAgICAgICBwYWdlX2lkID0gbXNnLmdldCgicGFnZV9pZCIpCiAgICAgICAgYXJncyA9IHBhcmFtcy5nZXQoImFyZ3MiLCBbXSkKICAgICAgICBrd2FyZ3MgPSBwYXJhbXMuZ2V0KCJrd2FyZ3MiLCB7fSkKICAgICAgICAKICAgICAgICByZXR1cm4gYXdhaXQgc2VsZi5icm93c2VyX2NvbnRyb2xsZXIucGxheXdyaWdodF9jb21tYW5kKAogICAgICAgICAgICBwYWdlX2lkLCBjb21tYW5kLCBhcmdzLCBrd2FyZ3MKICAgICAgICApCiAgICAKICAgIGFzeW5jIGRlZiBfaGFuZGxlX2NkcF9jb21tYW5kKHNlbGYsIG1zZzogRGljdFtzdHIsIEFueV0pIC0+IERpY3Rbc3RyLCBBbnldOgogICAgICAgICIiIkhhbmRsZSBDaHJvbWUgRGV2VG9vbHMgUHJvdG9jb2wgY29tbWFuZHMiIiIKICAgICAgICBjb21tYW5kID0gbXNnLmdldCgiY29tbWFuZCIpCiAgICAgICAgcGFyYW1zID0gbXNnLmdldCgicGFyYW1zIiwge30pCiAgICAgICAgcGFnZV9pZCA9IG1zZy5nZXQoInBhZ2VfaWQiKQogICAgICAgIAogICAgICAgIHJldHVybiBhd2FpdCBzZWxmLmJyb3dzZXJfY29udHJvbGxlci5jZHBfY29tbWFuZCgKICAgICAgICAgICAgcGFnZV9pZCwgY29tbWFuZCwgcGFyYW1zCiAgICAgICAgKQogICAgCiAgICBhc3luYyBkZWYgX3NlbmRfcmVzcG9uc2Uoc2VsZiwgY21kX2lkOiBzdHIsIHJlc3VsdF90eXBlOiBzdHIsICoqa3dhcmdzKToKICAgICAgICAiIiJTZW5kIHJlc3BvbnNlIGJhY2sgdG8gZ2F0ZXdheSIiIgogICAgICAgIGlmIG5vdCBzZWxmLndzIG9yIHNlbGYud3Muc3RhdGUubmFtZSA9PSAiQ0xPU0VEIjoKICAgICAgICAgICAgbG9nZ2VyLmVycm9yKCJDYW5ub3Qgc2VuZCByZXNwb25zZTogV2ViU29ja2V0IGNsb3NlZCIpCiAgICAgICAgICAgIHJldHVybgogICAgICAgIAogICAgICAgIHJlc3BvbnNlID0gewogICAgICAgICAgICAidHlwZSI6ICJicm93c2VyX2NvbnRyb2xfcmVzcG9uc2UiLAogICAgICAgICAgICAiaWQiOiBjbWRfaWQsCiAgICAgICAgICAgICJyZXN1bHQiOiByZXN1bHRfdHlwZSwKICAgICAgICAgICAgKiprd2FyZ3MKICAgICAgICB9CiAgICAgICAgCiAgICAgICAgYXdhaXQgc2VsZi53cy5zZW5kKGpzb24uZHVtcHMocmVzcG9uc2UpKQogICAgICAgIGxvZ2dlci5kZWJ1ZyhmIlNlbnQgcmVzcG9uc2UgZm9yIGNtZCB7Y21kX2lkfToge3Jlc3VsdF90eXBlfSIpCiAgICAKICAgIGFzeW5jIGRlZiBfaGFuZGxlX2V4ZWMoc2VsZiwgbXNnOiBkaWN0KToKICAgICAgICAiIiJFeGVjdXRlIGNvbW1hbmQgdmlhIHNleGVjLnNoIiIiCiAgICAgICAgY21kX2lkID0gbXNnLmdldCgiaWQiKQogICAgICAgIGNvbW1hbmQgPSBtc2cuZ2V0KCJjb21tYW5kIiwgW10pCiAgICAgICAgdGltZW91dCA9IG1zZy5nZXQoInRpbWVvdXQiLCAzMCkKICAgICAgICBhcHByb3ZlZCA9IG1zZy5nZXQoImFwcHJvdmVkIiwgRmFsc2UpCiAgICAgICAgCiAgICAgICAgbG9nZ2VyLmluZm8oZiJFeGVjdXRpbmcgY29tbWFuZCB7Y21kX2lkfTogeycgJy5qb2luKGNvbW1hbmQpfSIpCiAgICAgICAgCiAgICAgICAgIyBCdWlsZCBzZXhlYyBjb21tYW5kCiAgICAgICAgc2V4ZWNfY21kID0gWwogICAgICAgICAgICBzZWxmLmNvbmZpZ1sic2V4ZWNfcGF0aCJdLAogICAgICAgICAgICAicnVuIiwKICAgICAgICAgICAgIi0tY29tbWFuZCIsCiAgICAgICAgICAgICIgIi5qb2luKGNvbW1hbmQpCiAgICAgICAgXQogICAgICAgIAogICAgICAgIGlmIGFwcHJvdmVkOgogICAgICAgICAgICBzZXhlY19jbWQuYXBwZW5kKCItLWFwcHJvdmVkIikKICAgICAgICAKICAgICAgICB0cnk6CiAgICAgICAgICAgICMgRXhlY3V0ZSBjb21tYW5kCiAgICAgICAgICAgIHByb2Nlc3MgPSBhd2FpdCBhc3luY2lvLmNyZWF0ZV9zdWJwcm9jZXNzX2V4ZWMoCiAgICAgICAgICAgICAgICAqc2V4ZWNfY21kLAogICAgICAgICAgICAgICAgc3Rkb3V0PWFzeW5jaW8uc3VicHJvY2Vzcy5QSVBFLAogICAgICAgICAgICAgICAgc3RkZXJyPWFzeW5jaW8uc3VicHJvY2Vzcy5QSVBFCiAgICAgICAgICAgICkKICAgICAgICAgICAgCiAgICAgICAgICAgICMgU3RyZWFtIG91dHB1dAogICAgICAgICAgICBhc3luYyBkZWYgc3RyZWFtX291dHB1dChzdHJlYW0sIHN0cmVhbV9uYW1lKToKICAgICAgICAgICAgICAgIHdoaWxlIFRydWU6CiAgICAgICAgICAgICAgICAgICAgbGluZSA9IGF3YWl0IHN0cmVhbS5yZWFkbGluZSgpCiAgICAgICAgICAgICAgICAgICAgaWYgbm90IGxpbmU6CiAgICAgICAgICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgb3V0cHV0X21zZyA9IHsKICAgICAgICAgICAgICAgICAgICAgICAgInR5cGUiOiAiZXhlY19vdXRwdXQiLAogICAgICAgICAgICAgICAgICAgICAgICAiaWQiOiBjbWRfaWQsCiAgICAgICAgICAgICAgICAgICAgICAgICJzdHJlYW0iOiBzdHJlYW1fbmFtZSwKICAgICAgICAgICAgICAgICAgICAgICAgImRhdGEiOiBsaW5lLmRlY29kZSgndXRmLTgnLCBlcnJvcnM9J3JlcGxhY2UnKQogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICBpZiBzZWxmLndzIGFuZCBub3Qgc2VsZi53cy5zdGF0ZS5uYW1lID09ICJDTE9TRUQiOgogICAgICAgICAgICAgICAgICAgICAgICBhd2FpdCBzZWxmLndzLnNlbmQoanNvbi5kdW1wcyhvdXRwdXRfbXNnKSkKICAgICAgICAgICAgCiAgICAgICAgICAgICMgU3RyZWFtIHN0ZG91dCBhbmQgc3RkZXJyIGNvbmN1cnJlbnRseQogICAgICAgICAgICBhd2FpdCBhc3luY2lvLmdhdGhlcigKICAgICAgICAgICAgICAgIHN0cmVhbV9vdXRwdXQocHJvY2Vzcy5zdGRvdXQsICJzdGRvdXQiKSwKICAgICAgICAgICAgICAgIHN0cmVhbV9vdXRwdXQocHJvY2Vzcy5zdGRlcnIsICJzdGRlcnIiKQogICAgICAgICAgICApCiAgICAgICAgICAgIAogICAgICAgICAgICAjIFdhaXQgZm9yIGNvbXBsZXRpb24KICAgICAgICAgICAgZXhpdF9jb2RlID0gYXdhaXQgcHJvY2Vzcy53YWl0KCkKICAgICAgICAgICAgCiAgICAgICAgICAgICMgU2VuZCBjb21wbGV0aW9uIG1lc3NhZ2UKICAgICAgICAgICAgY29tcGxldGVfbXNnID0gewogICAgICAgICAgICAgICAgInR5cGUiOiAiZXhlY19jb21wbGV0ZSIsCiAgICAgICAgICAgICAgICAiaWQiOiBjbWRfaWQsCiAgICAgICAgICAgICAgICAiZXhpdF9jb2RlIjogZXhpdF9jb2RlCiAgICAgICAgICAgIH0KICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIHNlbGYud3MgYW5kIG5vdCBzZWxmLndzLnN0YXRlLm5hbWUgPT0gIkNMT1NFRCI6CiAgICAgICAgICAgICAgICBhd2FpdCBzZWxmLndzLnNlbmQoanNvbi5kdW1wcyhjb21wbGV0ZV9tc2cpKQogICAgICAgICAgICAKICAgICAgICAgICAgbG9nZ2VyLmluZm8oZiJDb21tYW5kIHtjbWRfaWR9IGNvbXBsZXRlZCB3aXRoIGV4aXQgY29kZSB7ZXhpdF9jb2RlfSIpCiAgICAgICAgICAgIAogICAgICAgIGV4Y2VwdCBhc3luY2lvLlRpbWVvdXRFcnJvcjoKICAgICAgICAgICAgbG9nZ2VyLmVycm9yKGYiQ29tbWFuZCB7Y21kX2lkfSB0aW1lZCBvdXQgYWZ0ZXIge3RpbWVvdXR9cyIpCiAgICAgICAgICAgIAogICAgICAgICAgICB0aW1lb3V0X21zZyA9IHsKICAgICAgICAgICAgICAgICJ0eXBlIjogImV4ZWNfY29tcGxldGUiLAogICAgICAgICAgICAgICAgImlkIjogY21kX2lkLAogICAgICAgICAgICAgICAgImV4aXRfY29kZSI6IC0xLAogICAgICAgICAgICAgICAgImVycm9yIjogInRpbWVvdXQiCiAgICAgICAgICAgIH0KICAgICAgICAgICAgCiAgICAgICAgICAgIGlmIHNlbGYud3MgYW5kIG5vdCBzZWxmLndzLnN0YXRlLm5hbWUgPT0gIkNMT1NFRCI6CiAgICAgICAgICAgICAgICBhd2FpdCBzZWxmLndzLnNlbmQoanNvbi5kdW1wcyh0aW1lb3V0X21zZykpCiAgICAgICAgICAgICAgICAKICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgIGxvZ2dlci5lcnJvcihmIkNvbW1hbmQge2NtZF9pZH0gZmFpbGVkOiB7ZX0iKQogICAgICAgICAgICAKICAgICAgICAgICAgZXJyb3JfbXNnID0gewogICAgICAgICAgICAgICAgInR5cGUiOiAiZXhlY19jb21wbGV0ZSIsCiAgICAgICAgICAgICAgICAiaWQiOiBjbWRfaWQsCiAgICAgICAgICAgICAgICAiZXhpdF9jb2RlIjogLTEsCiAgICAgICAgICAgICAgICAiZXJyb3IiOiBzdHIoZSkKICAgICAgICAgICAgfQogICAgICAgICAgICAKICAgICAgICAgICAgaWYgc2VsZi53cyBhbmQgbm90IHNlbGYud3Muc3RhdGUubmFtZSA9PSAiQ0xPU0VEIjoKICAgICAgICAgICAgICAgIGF3YWl0IHNlbGYud3Muc2VuZChqc29uLmR1bXBzKGVycm9yX21zZykpCiAgICAKICAgIGFzeW5jIGRlZiBfaGFuZGxlX2NhbmNlbChzZWxmLCBtc2c6IGRpY3QpOgogICAgICAgICIiIkhhbmRsZSBjb21tYW5kIGNhbmNlbGxhdGlvbiAobm90IGltcGxlbWVudGVkIHlldCkiIiIKICAgICAgICBjbWRfaWQgPSBtc2cuZ2V0KCJpZCIpCiAgICAgICAgbG9nZ2VyLndhcm5pbmcoZiJDb21tYW5kIGNhbmNlbGxhdGlvbiBub3QgeWV0IGltcGxlbWVudGVkIGZvciB7Y21kX2lkfSIpCiAgICAKICAgIGFzeW5jIGRlZiBydW4oc2VsZik6CiAgICAgICAgIiIiTWFpbiBydW4gbG9vcCB3aXRoIGF1dG8tcmVjb25uZWN0IiIiCiAgICAgICAgc2VsZi5ydW5uaW5nID0gVHJ1ZQogICAgICAgIAogICAgICAgIHdoaWxlIHNlbGYucnVubmluZzoKICAgICAgICAgICAgaWYgYXdhaXQgc2VsZi5jb25uZWN0KCk6CiAgICAgICAgICAgICAgICB0cnk6CiAgICAgICAgICAgICAgICAgICAgIyBTdGFydCBoZWFydGJlYXQgdGFzawogICAgICAgICAgICAgICAgICAgIGhlYXJ0YmVhdF90YXNrID0gYXN5bmNpby5jcmVhdGVfdGFzayhzZWxmLl9zZW5kX2hlYXJ0YmVhdCgpKQogICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICMgTWVzc2FnZSByZWNlaXZlIGxvb3AKICAgICAgICAgICAgICAgICAgICBhc3luYyBmb3IgbWVzc2FnZSBpbiBzZWxmLndzOgogICAgICAgICAgICAgICAgICAgICAgICBhd2FpdCBzZWxmLl9oYW5kbGVfbWVzc2FnZShtZXNzYWdlKQogICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICMgQ29ubmVjdGlvbiBjbG9zZWQKICAgICAgICAgICAgICAgICAgICBsb2dnZXIud2FybmluZygiQ29ubmVjdGlvbiBjbG9zZWQgYnkgZ2F0ZXdheSIpCiAgICAgICAgICAgICAgICAgICAgaGVhcnRiZWF0X3Rhc2suY2FuY2VsKCkKICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgIGV4Y2VwdCB3ZWJzb2NrZXRzLmV4Y2VwdGlvbnMuQ29ubmVjdGlvbkNsb3NlZCBhcyBlOgogICAgICAgICAgICAgICAgICAgIGxvZ2dlci53YXJuaW5nKGYiQ29ubmVjdGlvbiBjbG9zZWQ6IHtlfSIpCiAgICAgICAgICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgICAgICAgICAgICAgbG9nZ2VyLmVycm9yKGYiRXJyb3IgaW4gcnVuIGxvb3A6IHtlfSIpCiAgICAgICAgICAgIAogICAgICAgICAgICBpZiBzZWxmLnJ1bm5pbmc6CiAgICAgICAgICAgICAgICBsb2dnZXIuaW5mbyhmIlJlY29ubmVjdGluZyBpbiB7c2VsZi5yZWNvbm5lY3RfZGVsYXl9cy4uLiIpCiAgICAgICAgICAgICAgICBhd2FpdCBhc3luY2lvLnNsZWVwKHNlbGYucmVjb25uZWN0X2RlbGF5KQogICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAjIEV4cG9uZW50aWFsIGJhY2tvZmYKICAgICAgICAgICAgICAgIHNlbGYucmVjb25uZWN0X2RlbGF5ID0gbWluKAogICAgICAgICAgICAgICAgICAgIHNlbGYucmVjb25uZWN0X2RlbGF5ICogMiwKICAgICAgICAgICAgICAgICAgICBzZWxmLm1heF9yZWNvbm5lY3RfZGVsYXkKICAgICAgICAgICAgICAgICkKICAgIAogICAgZGVmIHN0b3Aoc2VsZik6CiAgICAgICAgIiIiU3RvcCB0aGUgYWdlbnQiIiIKICAgICAgICBsb2dnZXIuaW5mbygiU3RvcHBpbmcgYWdlbnQuLi4iKQogICAgICAgIHNlbGYucnVubmluZyA9IEZhbHNlCiAgICAgICAgaWYgc2VsZi53czoKICAgICAgICAgICAgYXN5bmNpby5jcmVhdGVfdGFzayhzZWxmLndzLmNsb3NlKCkpCgoKZGVmIG1haW4oKToKICAgICIiIk1haW4gZW50cnkgcG9pbnQiIiIKICAgIGltcG9ydCBzaWduYWwKICAgIAogICAgIyBQYXJzZSBjb21tYW5kIGxpbmUgYXJncwogICAgcGFyc2VyID0gYXJncGFyc2UuQXJndW1lbnRQYXJzZXIoZGVzY3JpcHRpb249Ikhlcm1lcyBOb2RlIEFnZW50IikKICAgIHBhcnNlci5hZGRfYXJndW1lbnQoIi0tY29uZmlnIiwgZGVmYXVsdD0iL2V0Yy9oZXJtZXMtbm9kZS9jb25maWcuanNvbiIsCiAgICAgICAgICAgICAgICAgICAgICAgaGVscD0iUGF0aCB0byBjb25maWcgZmlsZSAoZGVmYXVsdDogJShkZWZhdWx0KXMpIikKICAgIGFyZ3MgPSBwYXJzZXIucGFyc2VfYXJncygpCiAgICAKICAgICMgQ3JlYXRlIGFnZW50CiAgICBhZ2VudCA9IE5vZGVBZ2VudChhcmdzLmNvbmZpZykKICAgIAogICAgIyBIYW5kbGUgc2lnbmFscwogICAgZGVmIHNpZ25hbF9oYW5kbGVyKHNpZywgZnJhbWUpOgogICAgICAgIGxvZ2dlci5pbmZvKGYiUmVjZWl2ZWQgc2lnbmFsIHtzaWd9LCBzaHV0dGluZyBkb3duLi4uIikKICAgICAgICBhZ2VudC5zdG9wKCkKICAgIAogICAgc2lnbmFsLnNpZ25hbChzaWduYWwuU0lHSU5ULCBzaWduYWxfaGFuZGxlcikKICAgIHNpZ25hbC5zaWduYWwoc2lnbmFsLlNJR1RFUk0sIHNpZ25hbF9oYW5kbGVyKQogICAgCiAgICAjIFJ1biBhZ2VudAogICAgdHJ5OgogICAgICAgIGFzeW5jaW8ucnVuKGFnZW50LnJ1bigpKQogICAgZXhjZXB0IEtleWJvYXJkSW50ZXJydXB0OgogICAgICAgIGxvZ2dlci5pbmZvKCJJbnRlcnJ1cHRlZCBieSB1c2VyIikKICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToKICAgICAgICBsb2dnZXIuZXJyb3IoZiJGYXRhbCBlcnJvcjoge2V9IikKICAgICAgICBzeXMuZXhpdCgxKQoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBtYWluKCkK" | base64 -d > /tmp/hermes_install/hermes_node_agent.py
python3 -m py_compile /tmp/hermes_install/hermes_node_agent.py 2>/dev/null || {
echo -e "${RED}✗ Agent corrupted${NC}"; exit 1;
}
sudo cp /tmp/hermes_install/hermes_node_agent.py /usr/local/bin/hermes-node-agent
sudo chmod 755 /usr/local/bin/hermes-node-agent
echo -e " ${GREEN}✓ Installed${NC}"
# 5. Config
echo ""
echo -e "${CYAN}[5/6] Config...${NC}"
sudo mkdir -p /etc/hermes-node
sudo tee /etc/hermes-node/config.json > /dev/null <<EOF
{
"gateway_url": "wss://$GATEWAY_HOST:$GATEWAY_PORT",
"node_name": "$NODE_NAME",
"token": "$NODE_TOKEN",
"sexec_path": "$SEXEC_PATH",
"reconnect_interval": 5,
"heartbeat_interval": 30
}
EOF
sudo chown $USER:$USER /etc/hermes-node/config.json
echo -e " ${GREEN}✓ /etc/hermes-node/config.json${NC}"
# 6. Init service (optional)
echo ""
read -p "Install SysV init service? (yes/no): " INSTALL_INIT
if [ "$INSTALL_INIT" = "yes" ]; then
echo -e "${CYAN}[6/6] Init service...${NC}"
RUN_AS_USER=$(logname 2>/dev/null || echo "$SUDO_USER" || echo "$USER")
sudo tee /etc/init.d/hermes-node-agent > /dev/null <<'INITEOF'
#!/bin/bash
### BEGIN INIT INFO
# Provides: hermes-node-agent
# Required-Start: \$network \$local_fs
# Required-Stop: \$network \$local_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
### END INIT INFO
DAEMON=/usr/local/bin/hermes-node-agent
PIDFILE=/var/run/hermes-node-agent.pid
LOGFILE=/var/log/hermes-node-agent.log
USER='$RUN_AS_USER'
start() {
[ -f "\$PIDFILE" ] && ps -p \$(cat "\$PIDFILE") >/dev/null 2>&1 && { echo "Already running"; return 0; }
touch "\$LOGFILE"
nohup su - "\$USER" -c "\$DAEMON" >> "\$LOGFILE" 2>&1 &
echo \$! > "\$PIDFILE"
sleep 1
echo "Started"
}
stop() {
[ -f "\$PIDFILE" ] && kill \$(cat "\$PIDFILE") 2>/dev/null && rm -f "\$PIDFILE"
echo "Stopped"
}
status() {
if [ -f "\$PIDFILE" ] && ps -p \$(cat "\$PIDFILE") >/dev/null 2>&1; then
echo "Running (PID \$(cat \$PIDFILE))"
return 0
fi
echo "Stopped"
return 3
}
case "\$1" in
start) start ;;
stop) stop ;;
restart) stop; sleep 1; start ;;
status) status ;;
*) echo "Usage: \$0 {start|stop|restart|status}"; exit 1 ;;
esac
exit 0
INITEOF
sudo chmod 755 /etc/init.d/hermes-node-agent
command -v update-rc.d &>/dev/null && sudo update-rc.d hermes-node-agent defaults
command -v chkconfig &>/dev/null && sudo chkconfig --add hermes-node-agent
echo -e " ${GREEN}✓ Init service${NC}"
else
echo -e " ${YELLOW}Skipped${NC}"
fi
# 7. Validate
echo ""
echo -e "${CYAN}[7/7] Validate...${NC}"
python3 -m py_compile /usr/local/bin/hermes-node-agent 2>/dev/null && echo -e " ${GREEN}✓ Syntax${NC}"
[ -f "$SEXEC_PATH" ] && echo -e " ${GREEN}✓ sexec${NC}" || echo -e " ${YELLOW}⚠ sexec missing${NC}"
python3 -c "import json; json.load(open('/etc/hermes-node/config.json'))" 2>/dev/null && echo -e " ${GREEN}✓ JSON${NC}"
echo ""
echo -e "${GREEN}══════════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} ✓ INSTALLATION COMPLETE${NC}"
echo -e "${GREEN}══════════════════════════════════════════════════════════════${NC}"
echo ""
sudo cat /etc/hermes-node/config.json | python3 -m json.tool
echo ""
echo "Commands:"
echo " Start: sudo /etc/init.d/hermes-node-agent start"
echo " Status: sudo /etc/init.d/hermes-node-agent status"
echo " Logs: sudo tail -f /var/log/hermes-node-agent.log"
echo ""
<?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
# Hermes Browser Node Agent - Complete Deployment Guide
## Overview
The Hermes Browser Extension is a **stand-alone node agent** that runs entirely in your browser. It connects directly to your Hermes gateway via WebSocket (WSS), requiring **no software installation** on the host machine beyond Chrome/Edge and the extension itself.
## What's Included
### Core Extension Files
- `manifest.json` - MV3 Chrome extension manifest (with debugger permission)
- `background.js` - Service worker maintaining WebSocket connection
- `popup.html/popup.js` - Configuration UI (toolbar popup)
- `content-inject.js` - Content script bridge (injected into all pages)
- `injected.js` - Page-level API exposed as `window.HermesAgent`
- `cdp.js` - Chrome DevTools Protocol support module
- `icons/` - Extension icons (16x16, 48x48, 128x128)
### Build/Distribution Files
- `package.py` - Creates .zip and signed .crx packages
- `install.sh` - Installation helper script
- `update.xml` - Auto-update manifest
### Documentation
- `README.md` - Full usage guide for end-users
- `INTEGRATION_COMPLETE.md` - Technical integration docs
---
## Quick Start
### 1. Load the Extension
#### Option A: Developer Mode (Testing)
1. Open Chrome/Edge → `chrome://extensions/`
2. Enable **Developer mode** (top-right toggle)
3. Click **"Load unpacked"**
4. Select: `~/.hermes/plugins/hermes-node-gateway/hermes_browser_extension/`
5. Extension icon appears in your toolbar ✓
#### Option B: Signed CRX (Production)
```bash
cd /home/lisa/.hermes/plugins/hermes-node-gateway/hermes_browser_extension/
python3 package.py
```
This creates:
- `dist/hermes-browser-node-agent.zip` - Developer mode install
- `dist/hermes-browser-node-agent.crx` - Signed package (drag-and-drop install)
- `dist/hermes-browser-node-agent.pem` - Private key (keep safe!)
#### Option C: Auto-Install
```bash
cd /home/lisa/.hermes/plugins/hermes-node-gateway/hermes_browser_extension/
./install.sh
```
### 2. Configure Connection
1. Click the 🧠 Hermes icon in your toolbar
2. Enter your gateway details:
- **Gateway URL**: `wss://zeiss:8765` (or your gateway host)
- **Node Name**: `browser-laptop` (any unique name)
- **Authentication Token**: From `/etc/hermes-node-gateway/config.json`
3. Click **"Save & Connect"**
4. Status shows "✅ Connected" ✅
---
## Verifying Installation
### From the Extension
- Toolbar icon shows status:
- 🟢 **✅** = Connected
- 🔴 **✗** = Disconnected
- 🟠 **○** = Reconnecting
### From Hermes CLI
```bash
# List all nodes (including browser extension)
hermes node_list
# Expected output:
# - Name: browser-laptop
# - Capabilities: ["browser", "tabs", "scripting", "inject"]
# - Status: connected
```
### From Python API
```python
from hermes_agent import hermes_agent
# Get node list
nodes = hermes_agent.node_list()
print(nodes)
# {'nodes': [{'name': 'browser-laptop', 'status': 'connected', ...}]}
```
---
## Using the Extension
### Basic Browser Control
```python
# List all open tabs
browser_control(
node_name="browser-laptop",
command="list_tabs"
)
# Navigate to a URL
browser_control(
node_name="browser-laptop",
command="navigate",
page_id="tab_123",
params={"url": "https://github.com"}
)
# Click a button
browser_control(
node_name="browser-laptop",
command="click",
page_id="tab_123",
params={"selector": "button.submit"}
)
# Fill a form
browser_control(
node_name="browser-laptop",
command="fill",
page_id="tab_123",
params={
"selector": "input[name='email']",
"value": "user@example.com"
}
)
# Take screenshot
browser_control(
node_name="browser-laptop",
command="screenshot",
page_id="tab_123",
params={"format": "png", "full_page": True}
)
# Get page HTML
browser_control(
node_name="browser-laptop",
command="get_content",
page_id="tab_123"
)
```
### JavaScript Execution
```python
# Evaluate JavaScript in page context
browser_control(
node_name="browser-laptop",
command="evaluate",
page_id="tab_123",
params={
"expression": "document.querySelectorAll('a').length"
}
)
# Returns: {'result': 42}
```
### Page-level API (from Page JavaScript)
Inject the extension into any page — it exposes `window.HermesAgent`:
```javascript
// Wait for extension to be ready
window.addEventListener('hermes:ready', function() {
console.log('Hermes API loaded!');
// Get page info
window.HermesAgent.getPageInfo().then(info => {
console.log('URL:', info.url);
console.log('Title:', info.title);
});
// Fill a form
window.HermesAgent.fillField('input[name="search"]', 'hermes');
// Click element
window.HermesAgent.clickElement('button#submit');
// Wait for selector
window.HermesAgent.waitForSelector('.results')
.then(el => console.log('Found results:', el));
// Monitor DOM changes
const observer = window.HermesAgent.observeMutations(
'#container',
(mutations) => console.log('DOM changed:', mutations)
);
// Get all cookies
window.HermesAgent.getCookies().then(cookies => {
console.log('Cookies:', cookies);
});
});
```
### Chrome DevTools Protocol (CDP)
```python
# Network monitoring
browser_control(
node_name="browser-laptop",
command="Network.enable",
layer="cdp",
page_id="tab_123"
)
# Get DOM document
browser_control(
node_name="browser-laptop",
command="DOM.getDocument",
layer="cdp",
page_id="tab_123"
)
# Evaluate in isolated world
browser_control(
node_name="browser-laptop",
command="Runtime.evaluate",
layer="cdp",
page_id="tab_123",
params={
"expression": "1 + 2 + 3",
"returnByValue": True
}
)
```
---
## Configuration Options
### Gateway Configuration
**Gateway Host** (any accessible from browser):
- `wss://zeiss:8765` (if on same network)
- `wss://192.168.1.100:8765` (specific IP)
- `wss://gateway.example.com:8765` (with reverse proxy)
**Node Name**: Any unique identifier
- `browser-laptop`
- `firefox-office`
- `chromium-testing`
**Token**: Must match gateway config
Add tokens to `/etc/hermes-node-gateway/config.json`:
```json
{
"tokens": {
"sissy": "token-for-python-agent",
"browser-laptop": "token-for-browser-extension"
}
}
```
Restart gateway after changes:
```bash
sudo systemctl restart hermes-node-gateway
# or
./scripts/restart-gateway.sh
```
### Extension Settings
Access settings anytime via the extension popup. Changes apply immediately.
---
## Security
### Transport Security
- **All connections use WSS** (WebSocket over TLS)
- Self-signed certificates are supported (verify in settings)
- Certificate expiry is checked automatically
### Authentication
- **Token-based authentication** required for all nodes
- Tokens are stored in `chrome.storage.local` (encrypted)
- No plaintext tokens in code or config files
### Permissions
| Permission | Purpose |
|------------|---------|
| `tabs` | List, create, close tabs |
| `scripting` | Inject JavaScript into pages |
| `webNavigation` | Monitor page loads |
| `alarms` | Periodic heartbeats |
| `debugger` | Chrome DevTools Protocol (optional) |
| `declarativeNetRequest` | Request interception |
| `storage` | Save configuration |
**No permissions granted to:**
-`geolocation`
-`cookies`
-`downloads`
-`history`
- ❌ Unrestricted host access (only active tabs)
### Isolation
- **Content scripts run in isolated world** — cannot access page JavaScript
- **Page scripts cannot access extension** — communication via postMessage only
- **No file system access** — all data stored in browser storage
- **No network access** — only to your specified WebSocket gateway
---
## Troubleshooting
### Extension Won't Connect
**Symptom**: Status shows "✗ Disconnected" or "○ Reconnecting"
**Solutions**:
1. Check gateway URL
```bash
# Can you reach the gateway?
curl -k https://your-gateway:8765 # Should get 400 or 404
```
2. Verify token
```bash
# Check token in gateway config
cat /etc/hermes-node-gateway/config.json | grep -A 5 tokens
```
3. Check WebSocket port
```bash
ss -tlnp | grep 8765
# Should show your gateway process listening
```
4. Test with Python client
```python
import websockets
import asyncio
async def test():
uri = "wss://your-gateway:8765/nodes?token=YOURTOKEN"
try:
async with websockets.connect(uri) as ws:
print("Connected!")
except Exception as e:
print(f"Error: {e}")
asyncio.run(test())
```
5. Check browser console
- Right-click extension icon → Inspect popup
- Go to Console tab
- Look for error messages
### Commands Not Working
**Symptom**: Browser control commands fail or timeout
**Solutions**:
1. Verify node is connected
```python
node_list() # Should show your node as "connected"
```
2. Get valid page_id
```python
browser_control(node_name="browser-laptop", command="list_tabs")
```
3. Test with simple command
```python
browser_control(node_name="browser-laptop", command="get_title", page_id="tab_123")
```
4. Check page content policy
- Some sites (google.com, github.com) block scripting
- Try on `about:blank` first
### Extension Disappears
**Symptom**: Extension icon vanishes after restart
**Solution**:
- Chrome disables extensions that crash on startup
- Go to `chrome://extensions/`
- Enable "Developer mode"
- Click "Reload" on the Hermes extension
- Check "Inspect views: service worker" for errors
### Memory Leaks
**Symptom**: Extension uses too much memory
**Solution**:
- Service workers sleep when inactive
- Memory is released automatically
- If problem persists, reload extension in `chrome://extensions/`
---
## Maintenance
### Updating the Extension
**Developer Mode**:
1. Make code changes
2. Go to `chrome://extensions/`
3. Click "Reload" on Hermes extension
**CRX Package**:
1. Update version in `manifest.json`
2. Run `python3 package.py`
3. Replace `.crx` on your server
4. Users auto-update (if using update manifest)
### Backing Up Configuration
Configuration is stored in Chrome's profile:
```bash
# Export config
chrome.storage.local.get(null, function(items) {
console.log(JSON.stringify(items, null, 2));
});
```
Or copy Chrome profile directory:
- Linux: `~/.config/google-chrome/Default/Local Extension Settings/`
- Windows: `%USERPROFILE%\AppData\Local\Google\Chrome\User Data\Default\Local Extension Settings\`
### Checking Logs
**Background script logs**:
1. Go to `chrome://extensions/`
2. Find Hermes extension
3. Click "Inspect views: service worker"
4. Check Console tab
**Content script logs**:
1. Open any page where extension is active
2. Press F12 for DevTools
3. Check Console tab for `[Hermes]` messages
**Node agent logs**:
```bash
tail -f /var/log/hermes-node-agent.log
```
---
## Advanced Usage
### Headless Operation
Run Chrome in headless mode with extension:
```bash
google-chrome \
--headless \
--disable-gpu \
--remote-debugging-port=9222 \
--load-extension=/path/to/hermes_browser_extension
```
Then connect to `ws://localhost:9222` via browser automation.
### Multiple Profiles
Use different Chrome profiles for different gateway connections:
```bash
# Create profile for work
mkdir -p ~/.config/google-chrome-work
# Launch with profile
chromium \
--user-data-dir=~/.config/google-chrome-work \
--load-extension=/path/to/hermes_browser_extension
```
### Enterprise Deployment
Deploy extension via Chrome Web Store or Microsoft Store:
1. Package extension: `python3 package.py`
2. Upload `.zip` to Chrome Web Store Developer Dashboard
3. Set to "Unlisted" for controlled distribution
4. Users install from `https://chrome.google.com/webstore/...`
---
## Integration with Python Node Agent
The browser extension complements (doesn't replace) the Python node agent:
| Feature | Python Agent | Browser Extension |
|---------|-------------|-------------------|
| Shell commands | ✅ Full (`exec`) | ❌ Not available |
| Playwright | ✅ Full | ❌ Not available |
| CDP access | ✅ Full | ✅ Via `chrome.debugger` |
| Extensions | ✅ Can load | ⚠️ Self only |
| Installation | ❌ Requires Python | ✅ One click |
| Linux/Windows/Mac | ✅ All | ✅ All |
| Headless | ✅ Yes | ✅ Yes |
| Mobile | ❌ No | ❌ No |
**Use both**: Register node extension in gateway config to work together!
---
## Contributing
### Adding Commands
**Background script** (`background.js`):
1. Add command to `executeHighLevelCommand()` switch
2. Implement logic
3. Return `{ success: true, ... }` or `{ success: false, error: '...' }`
**Content script** (`injected.js`):
1. Add method to `window.HermesAgent`
2. Document API
3. Dispatch `hermes:ready` event on completion
### Testing
Run tests:
```bash
cd /home/lisa/hermes-node-protocol/node-agent
venv/bin/python3 test_browser.py
```
Test end-to-end:
```bash
# 1. Start gateway
./scripts/start-gateway.sh
# 2. Connect browser extension
# (Manual step)
# 3. Run tests from Hermes
hermes node_list
browser_control(node_name="browser-laptop", command="list_tabs")
```
---
## Support
For issues or questions:
1. Check [README.md](README.md) for common issues
2. Review [Troubleshooting](#troubleshooting) section
3. Check logs:
- Browser: `chrome://extensions/` → Inspect views
- Gateway: `journalctl -u hermes-node-gateway -f`
- Node: `tail -f /var/log/hermes-node-agent.log`
4. Contact: Open issue on GitHub repository
---
## Version History
### v2.0 (Current)
- 🎉 Standalone browser extension release
- 🔐 Chrome DevTools Protocol (CDP) support
- 📦 Signed CRX packages
- 🧪 JavaScript injection API
- 📝 Full documentation
### v1.0
- Initial release
- WebSocket client
- Basic browser control
- High-level commands
---
## License
Part of the Hermes Agent project.
[MIT License](LICENSE)
Copyright 2026
# 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.
/**
* Hermes Browser Node Agent - Background Service Worker
*
* Connects to Hermes Gateway via WebSocket and acts as a browser-based node agent.
* Provides browser automation, CDP access, and JavaScript injection without local software.
*
* Architecture:
* - Service Worker (this file) maintains WebSocket connection to gateway
* - Content scripts inject into pages for DOM manipulation
* - Chrome APIs provide browser control (tabs, scripting, etc.)
*/
const VERSION = '2.0';
const DEFAULT_GATEWAY_URL = 'wss://localhost:8765';
const RECONNECT_DELAY_MS = 5000;
const HEARTBEAT_INTERVAL_MS = 30000;
// Import CDP module
importScripts('cdp.js');
// State
let ws = null;
let connected = false;
let reconnectTimer = null;
let heartbeatTimer = null;
let config = null;
let pendingCommands = new Map(); // cmd_id -> {resolve, reject, timeout}
// Initialize on install/startup
chrome.runtime.onInstalled.addListener(() => {
console.log('[Hermes] Extension installed');
loadConfig();
});
chrome.runtime.onStartup.addListener(() => {
console.log('[Hermes] Browser started, initializing agent');
loadConfig();
});
// Load configuration from storage
async function loadConfig() {
const stored = await chrome.storage.local.get(['gateway_url', 'node_name', 'token']);
config = {
gateway_url: stored.gateway_url || DEFAULT_GATEWAY_URL,
node_name: stored.node_name || `browser-${generateNodeId()}`,
token: stored.token || ''
};
console.log('[Hermes] Config loaded:', {
gateway_url: config.gateway_url,
node_name: config.node_name,
has_token: !!config.token
});
// Auto-connect if token is set
if (config.token) {
connect();
} else {
console.log('[Hermes] No token configured. Open extension popup to configure.');
}
}
// Save configuration
async function saveConfig(newConfig) {
await chrome.storage.local.set(newConfig);
config = { ...config, ...newConfig };
console.log('[Hermes] Config saved');
}
// Generate unique node ID
function generateNodeId() {
return Math.random().toString(36).substring(2, 10);
}
// Connect to gateway
function connect() {
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) {
console.log('[Hermes] Already connected or connecting');
return;
}
if (!config.token) {
console.error('[Hermes] Cannot connect: no token configured');
return;
}
const url = `${config.gateway_url}/nodes?token=${config.token}`;
console.log('[Hermes] Connecting to gateway:', config.gateway_url);
try {
ws = new WebSocket(url);
ws.onopen = handleOpen;
ws.onmessage = handleMessage;
ws.onerror = handleError;
ws.onclose = handleClose;
} catch (error) {
console.error('[Hermes] Connection error:', error);
scheduleReconnect();
}
}
// Handle connection open
async function handleOpen() {
console.log('[Hermes] WebSocket connected');
connected = true;
// Send registration
const registration = {
type: 'register',
node_name: config.node_name,
version: VERSION,
capabilities: ['browser', 'tabs', 'scripting', 'cdp', 'inject'],
platform: 'browser_extension',
browser: getBrowserInfo()
};
send(registration);
// Start heartbeat
startHeartbeat();
// Update badge
chrome.action.setBadgeText({ text: '✓' });
chrome.action.setBadgeBackgroundColor({ color: '#00AA00' });
}
// Handle incoming messages
async function handleMessage(event) {
try {
const msg = JSON.parse(event.data);
console.log('[Hermes] Received:', msg.type, msg.id);
switch (msg.type) {
case 'register_ack':
console.log('[Hermes] Registration acknowledged, gateway version:', msg.gateway_version);
break;
case 'heartbeat_ack':
// Silent
break;
case 'browser_control':
await handleBrowserControl(msg);
break;
case 'exec':
// Browser extension can't execute shell commands
sendResponse(msg.id, 'error', { error: 'Browser nodes cannot execute shell commands' });
break;
case 'disconnect':
console.log('[Hermes] Gateway requested disconnect:', msg.reason);
disconnect();
break;
default:
console.warn('[Hermes] Unknown message type:', msg.type);
}
} catch (error) {
console.error('[Hermes] Error handling message:', error);
}
}
// Handle browser control commands
async function handleBrowserControl(msg) {
const { id, command, layer, page_id, params } = msg;
try {
let result;
switch (layer || 'high_level') {
case 'high_level':
result = await executeHighLevelCommand(command, page_id, params || {});
break;
case 'cdp':
result = await executeCDPCommand(command, page_id, params || {});
break;
case 'inject':
result = await executeInjection(command, page_id, params || {});
break;
default:
result = { success: false, error: `Unknown layer: ${layer}` };
}
sendResponse(id, result.success ? 'ok' : 'error', result);
} catch (error) {
console.error('[Hermes] Browser control error:', error);
sendResponse(id, 'error', { error: error.message });
}
}
// Execute high-level browser commands
async function executeHighLevelCommand(command, page_id, params) {
const tabId = page_id ? parseInt(page_id.replace('tab_', '')) : null;
switch (command) {
case 'list_tabs':
const tabs = await chrome.tabs.query({});
return {
success: true,
tabs: tabs.map(t => ({
page_id: `tab_${t.id}`,
url: t.url,
title: t.title,
active: t.active,
windowId: t.windowId
}))
};
case 'create_tab':
const newTab = await chrome.tabs.create({
url: params.url || 'about:blank',
active: params.active !== false
});
return {
success: true,
page_id: `tab_${newTab.id}`,
url: newTab.url
};
case 'navigate':
if (!tabId) return { success: false, error: 'page_id required' };
await chrome.tabs.update(tabId, { url: params.url });
// Wait for load
await waitForTabLoad(tabId);
const tab = await chrome.tabs.get(tabId);
return {
success: true,
url: tab.url,
title: tab.title
};
case 'close_tab':
if (!tabId) return { success: false, error: 'page_id required' };
await chrome.tabs.remove(tabId);
return { success: true };
case 'screenshot':
if (!tabId) return { success: false, error: 'page_id required' };
const dataUrl = await chrome.tabs.captureVisibleTab(null, {
format: params.format || 'png'
});
return {
success: true,
screenshot: dataUrl.split(',')[1], // base64 only
format: params.format || 'png',
encoding: 'base64'
};
case 'get_content':
if (!tabId) return { success: false, error: 'page_id required' };
const content = await executeScript(tabId, () => document.documentElement.outerHTML);
return {
success: true,
content: content[0].result
};
case 'get_title':
if (!tabId) return { success: false, error: 'page_id required' };
const titleTab = await chrome.tabs.get(tabId);
return {
success: true,
title: titleTab.title
};
case 'click':
if (!tabId) return { success: false, error: 'page_id required' };
await executeScript(tabId, (selector) => {
const el = document.querySelector(selector);
if (!el) throw new Error(`Element not found: ${selector}`);
el.click();
}, [params.selector]);
return { success: true, selector: params.selector };
case 'fill':
if (!tabId) return { success: false, error: 'page_id required' };
await executeScript(tabId, (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 }));
}, [params.selector, params.value]);
return { success: true, selector: params.selector, value: params.value };
case 'evaluate':
if (!tabId) return { success: false, error: 'page_id required' };
const evalResult = await executeScript(tabId, (expr) => eval(expr), [params.expression]);
return {
success: true,
result: evalResult[0].result
};
default:
return { success: false, error: `Unknown command: ${command}` };
}
}
// Execute CDP commands (now uses cdp.js module)
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' };
}
try {
// Use CDP module
return await self.executeCDPCommand(command, page_id, params);
} catch (error) {
return { success: false, error: error.message };
}
}
// Execute JavaScript injection
async function executeInjection(command, page_id, params) {
const tabId = page_id ? parseInt(page_id.replace('tab_', '')) : null;
if (!tabId) return { success: false, error: 'page_id required' };
switch (command) {
case 'inject_script':
const result = await executeScript(tabId, params.script);
return { success: true, result: result[0]?.result };
case 'inject_file':
await chrome.scripting.executeScript({
target: { tabId },
files: [params.file]
});
return { success: true };
default:
return { success: false, error: `Unknown inject command: ${command}` };
}
}
// Helper: execute script in tab
async function executeScript(tabId, func, args = []) {
return await chrome.scripting.executeScript({
target: { tabId },
func,
args
});
}
// Helper: wait for tab to finish loading
function waitForTabLoad(tabId, timeout = 30000) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
chrome.tabs.onUpdated.removeListener(listener);
reject(new Error('Tab load timeout'));
}, timeout);
const listener = (updatedTabId, changeInfo) => {
if (updatedTabId === tabId && changeInfo.status === 'complete') {
clearTimeout(timer);
chrome.tabs.onUpdated.removeListener(listener);
resolve();
}
};
chrome.tabs.onUpdated.addListener(listener);
});
}
// Send response back to gateway
function sendResponse(cmdId, resultType, data = {}) {
const response = {
type: 'browser_control_response',
id: cmdId,
result: resultType,
...data
};
send(response);
}
// Send message to gateway
function send(msg) {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(msg));
} else {
console.error('[Hermes] Cannot send: WebSocket not open');
}
}
// Start heartbeat
function startHeartbeat() {
if (heartbeatTimer) clearInterval(heartbeatTimer);
heartbeatTimer = setInterval(() => {
if (connected) {
send({
type: 'heartbeat',
timestamp: Date.now()
});
}
}, HEARTBEAT_INTERVAL_MS);
}
// Handle errors
function handleError(error) {
console.error('[Hermes] WebSocket error:', error);
chrome.action.setBadgeText({ text: '✗' });
chrome.action.setBadgeBackgroundColor({ color: '#AA0000' });
}
// Handle close
function handleClose(event) {
console.log('[Hermes] WebSocket closed:', event.code, event.reason);
connected = false;
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
chrome.action.setBadgeText({ text: '○' });
chrome.action.setBadgeBackgroundColor({ color: '#888888' });
// Auto-reconnect
scheduleReconnect();
}
// Schedule reconnection
function scheduleReconnect() {
if (reconnectTimer) return;
console.log(`[Hermes] Reconnecting in ${RECONNECT_DELAY_MS}ms...`);
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
connect();
}, RECONNECT_DELAY_MS);
}
// Disconnect
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
connected = false;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
if (heartbeatTimer) {
clearInterval(heartbeatTimer);
heartbeatTimer = null;
}
}
// Get browser info
function getBrowserInfo() {
const ua = navigator.userAgent;
let browser = 'unknown';
if (ua.includes('Chrome')) browser = 'chrome';
else if (ua.includes('Firefox')) browser = 'firefox';
else if (ua.includes('Safari')) browser = 'safari';
else if (ua.includes('Edge')) browser = 'edge';
return {
name: browser,
version: navigator.appVersion,
platform: navigator.platform
};
}
// Handle messages from popup
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
switch (msg.type) {
case 'get_status':
sendResponse({
connected,
config,
gateway_url: config?.gateway_url,
node_name: config?.node_name
});
break;
case 'update_config':
saveConfig(msg.config).then(() => {
disconnect();
connect();
sendResponse({ success: true });
});
return true; // async
case 'connect':
connect();
sendResponse({ success: true });
break;
case 'disconnect':
disconnect();
sendResponse({ success: true });
break;
}
});
console.log('[Hermes] Background service worker initialized');
/**
* 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