Skill: danielithomas/lobsterlan (version 1.0.1)

--- name: lobsterlan version: 1.0.1 description: Communicate with other OpenClaw agents on your local network. Use when you need to ask another agent a question (sync), delegate a task (async), or check if a peer agent is reachable. Supports both synchronous chat completions and asynchronous webhook-based task delegation. Requires peers.json config with peer addresses and tokens. --- # LobsterLAN — Agent-to-Agent Communication Talk to other OpenClaw agents on your LAN. ## Setup 1. Copy `config/peers.example.json` to `config/peers.json` 2. Fill in peer hostnames, ports, and tokens 3. Ensure target agents have the required APIs enabled (see below) 4. **Set up a secure transport** (see Network Transport below) ## Required Config on Target Agent For **sync ask** (chat completions): ```jsonc // Target agent's openclaw.json — keep bind as "loopback"! { "gateway": { "http": { "endpoints": { "chatCompletions": { "enabled": true } } } } } ``` > ⚠️ **Do NOT set `gateway.bind` to `"lan"`** — OpenClaw will refuse to start if the gateway is exposed on a non-loopback address without TLS. Use a secure transport instead (see below). For **async delegate** (webhooks): ```jsonc { "hooks": { "enabled": true, "token": "a-secure-shared-secret" } } ``` ## Network Transport OpenClaw gateways default to `bind: loopback` and will not start with plaintext on non-loopback addresses. You need a secure transport layer for cross-host communication: | Approach | Complexity | Best For | |----------|-----------|----------| | **SSH Tunnel** ⭐ | Low | Home LANs, simple setups | | **Reverse Proxy (TLS)** | Medium | Environments with existing Caddy/nginx | | **Tailscale Serve** | Medium | Multi-site or remote agents | **For simple LANs, SSH tunneling is recommended.** Both gateways stay on loopback, the SSH tunnel provides encryption, and no gateway config changes are needed. ### SSH Tunnel Example Forward a local port to the remote agent's loopback gateway: ```bash ssh -N -L 18790:127.0.0.1:18790 user@remote-agent-host ``` Then in `peers.json`, point the peer to `127.0.0.1:18790` (the local tunnel endpoint). For persistence, use a systemd user service with `Restart=always`. See the full setup guide in `docs/setup.md`. ## Commands ### Ask (synchronous — wait for reply) ```bash scripts/lobsterlan.sh ask scotty "What is the CPU temperature?" ``` Use for quick questions where you need the answer now. ### Delegate (async — fire and forget) ```bash scripts/lobsterlan.sh delegate scotty "Generate 5 zen wallpapers and push to the file share" ``` Use for long-running tasks. The peer processes independently. ### Status check ```bash scripts/lobsterlan.sh status scotty ``` ### List peers ```bash scripts/lobsterlan.sh peers ``` ## Agent Usage (from within OpenClaw) Run via exec tool: ```bash cd ~/.openclaw/workspace/skills/lobsterlan && scripts/lobsterlan.sh ask scotty "status report" ``` ## Security Three layers protect communication: 1. **Network**: LAN-only (firewall blocks external access to gateway port) 2. **Gateway token**: Bearer auth on every request 3. **Agent ID header** (optional): `X-LobsterLAN-Agent` sent with self-ID The gateway token is the real security boundary. The agent ID header is defense-in-depth for environments where you want explicit identity verification. ## Environment Variables - `LOBSTERLAN_CONFIG` — path to peers.json (default: `../config/peers.json` relative to script)

path: config/peers.example.json
content: {
  "_comment": "LobsterLAN peer configuration. Copy to peers.json and fill in your values.",
  "self": {
    "id": "spock",
    "name": "Spock",
    "host": "192.168.1.2",
    "port": 18789
  },
  "peers": {
    "scotty": {
      "name": "Scotty",
      "host": "192.168.1.3",
      "port": 18789,
      "gateway_token": "PEER_GATEWAY_TOKEN_HERE",
      "hooks_token": "PEER_HOOKS_TOKEN_HERE",
      "chat_completions": true,
      "webhooks": true
    }
  },
  "security": {
    "require_agent_id_header": false,
    "allowed_agents": ["spock", "scotty"]
  }
}


path: scripts/lobsterlan.sh
content: #!/usr/bin/env bash
# lobsterlan.sh — Agent-to-agent communication for OpenClaw on LAN
# Usage:
#   lobsterlan.sh ask <peer> <message>           # Sync ask via chat completions
#   lobsterlan.sh delegate <peer> <message>      # Async task via webhooks
#   lobsterlan.sh status <peer>                  # Check if peer is reachable
#   lobsterlan.sh peers                          # List configured peers

set -euo pipefail

# --- Config ---
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${LOBSTERLAN_CONFIG:-$SCRIPT_DIR/../config/peers.json}"

if [[ ! -f "$CONFIG_FILE" ]]; then
  echo "Error: Config file not found: $CONFIG_FILE" >&2
  echo "Copy peers.example.json to peers.json and configure your peers." >&2
  exit 1
fi

# --- Validation ---
validate_peer_name() {
  local peer="$1"
  if [[ ! "$peer" =~ ^[a-zA-Z0-9_-]+$ ]]; then
    echo "Error: Invalid peer name '$peer' — must match [a-zA-Z0-9_-]+" >&2
    exit 1
  fi
}

validate_host_port() {
  local host="$1" port="$2"
  if [[ ! "$host" =~ ^[a-zA-Z0-9._-]+$ ]]; then
    echo "Error: Invalid host '$host' — must match [a-zA-Z0-9._-]+" >&2
    exit 1
  fi
  if [[ ! "$port" =~ ^[0-9]+$ ]]; then
    echo "Error: Invalid port '$port' — must be numeric" >&2
    exit 1
  fi
}

warn_if_not_localhost() {
  local host="$1"
  if [[ "$host" != "localhost" && "$host" != "127.0.0.1" && "$host" != "::1" ]]; then
    echo "Warning: Connecting to non-localhost address '$host' over plaintext HTTP" >&2
  fi
}

# --- Helpers ---
get_peer_field() {
  local peer="$1" field="$2"
  LOBSTERLAN_CF="$CONFIG_FILE" LOBSTERLAN_PEER="$peer" LOBSTERLAN_FIELD="$field" \
  python3 -c "
import json, sys, os
with open(os.environ['LOBSTERLAN_CF']) as f:
    cfg = json.load(f)
p = cfg.get('peers', {}).get(os.environ['LOBSTERLAN_PEER'])
if not p:
    print('ERROR: Unknown peer: ' + os.environ['LOBSTERLAN_PEER'], file=sys.stderr)
    sys.exit(1)
print(p.get(os.environ['LOBSTERLAN_FIELD'], ''))
"
}

get_self_id() {
  LOBSTERLAN_CF="$CONFIG_FILE" \
  python3 -c "
import json, os
with open(os.environ['LOBSTERLAN_CF']) as f:
    cfg = json.load(f)
print(cfg.get('self', {}).get('id', 'unknown'))
"
}

# --- Commands ---
cmd_ask() {
  local peer="$1"
  shift
  local message="$*"

  validate_peer_name "$peer"

  local host; host=$(get_peer_field "$peer" "host")
  local port; port=$(get_peer_field "$peer" "port")
  local token; [REDACTED_SECRET] "$peer" "gateway_token")
  local self_id; self_id=$(get_self_id)

  if [[ -z "$host" || -z "$port" || -z "$token" ]]; then
    echo "Error: Incomplete config for peer '$peer'" >&2
    exit 1
  fi

  validate_host_port "$host" "$port"

  local url="http://${host}:${port}/v1/chat/completions"

  warn_if_not_localhost "$host"

  local response
  response=$(curl -sS --max-time 120 "$url" \
    -H "Authorization: Bearer ${token}" \
    -H "Content-Type: application/json" \
    -H "X-LobsterLAN-Agent: ${self_id}" \
    -d "$(LOBSTERLAN_SELF="$self_id" python3 -c "
import json, sys, os
msg = sys.stdin.read()
print(json.dumps({
    'model': 'openclaw:main',
    'user': os.environ['LOBSTERLAN_SELF'],
    'messages': [{'role': 'user', 'content': msg}]
}))
" <<< "$message")" 2>&1)

  local exit_code=$?
  if [[ $exit_code -ne 0 ]]; then
    echo "Error: Failed to reach $peer at $url" >&2
    echo "$response" >&2
    exit 1
  fi

  # Extract the reply text
  echo "$response" | python3 -c "
import json, sys
try:
    data = json.loads(sys.stdin.read())
    choices = data.get('choices', [])
    if choices:
        print(choices[0].get('message', {}).get('content', '(no content)'))
    else:
        print('(no response)')
except Exception as e:
    print(f'Error parsing response: {e}', file=sys.stderr)
    sys.stdin.seek(0)
    print(sys.stdin.read())
"
}

cmd_delegate() {
  local peer="$1"
  shift
  local message="$*"

  validate_peer_name "$peer"

  local host; host=$(get_peer_field "$peer" "host")
  local port; port=$(get_peer_field "$peer" "port")
  local token; [REDACTED_SECRET] "$peer" "hooks_token")
  local self_id; self_id=$(get_self_id)

  if [[ -z "$host" || -z "$port" || -z "$token" ]]; then
    echo "Error: Incomplete config for peer '$peer'" >&2
    exit 1
  fi

  validate_host_port "$host" "$port"

  local url="http://${host}:${port}/hooks/agent"

  warn_if_not_localhost "$host"

  local response
  response=$(curl -sS --max-time 10 "$url" \
    -H "Authorization: Bearer ${token}" \
    -H "Content-Type: application/json" \
    -H "X-LobsterLAN-Agent: ${self_id}" \
    -d "$(LOBSTERLAN_SELF="$self_id" python3 -c "
import json, sys, os
msg = sys.stdin.read()
print(json.dumps({
    'message': msg,
    'name': os.environ['LOBSTERLAN_SELF'],
    'wakeMode': 'now',
    'deliver': False
}))
" <<< "$message")" 2>&1)

  local exit_code=$?
  if [[ $exit_code -ne 0 ]]; then
    echo "Error: Failed to reach $peer at $url" >&2
    echo "$response" >&2
    exit 1
  fi

  echo "Task delegated to $peer (async)"
  echo "$response"
}

cmd_status() {
  local peer="$1"

  validate_peer_name "$peer"

  local host; host=$(get_peer_field "$peer" "host")
  local port; port=$(get_peer_field "$peer" "port")
  local token; [REDACTED_SECRET] "$peer" "gateway_token")

  validate_host_port "$host" "$port"

  local url="http://${host}:${port}/health"

  warn_if_not_localhost "$host"

  local http_code
  http_code=$(curl -sS --max-time 5 -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000")

  if [[ "$http_code" == "200" ]]; then
    echo "✅ $peer is reachable at ${host}:${port}"
  else
    echo "❌ $peer is unreachable at ${host}:${port} (HTTP $http_code)"
  fi
}

cmd_peers() {
  LOBSTERLAN_CF="$CONFIG_FILE" \
  python3 -c "
import json, os
with open(os.environ['LOBSTERLAN_CF']) as f:
    cfg = json.load(f)
self_info = cfg.get('self', {})
print(f\"Self: {self_info.get('id', '?')} ({self_info.get('name', '?')}) @ {self_info.get('host', '?')}:{self_info.get('port', '?')}\")
print()
for pid, p in cfg.get('peers', {}).items():
    cc = '✅' if p.get('chat_completions') else '❌'
    wh = '✅' if p.get('webhooks') else '❌'
    print(f\"{pid}: {p.get('name', '?')} @ {p.get('host', '?')}:{p.get('port', '?')} [completions:{cc} hooks:{wh}]\")
"
}

# --- Main ---
case "${1:-help}" in
  ask)
    shift
    if [[ $# -lt 2 ]]; then
      echo "Usage: lobsterlan.sh ask <peer> <message>" >&2
      exit 1
    fi
    cmd_ask "$@"
    ;;
  delegate)
    shift
    if [[ $# -lt 2 ]]; then
      echo "Usage: lobsterlan.sh delegate <peer> <message>" >&2
      exit 1
    fi
    cmd_delegate "$@"
    ;;
  status)
    shift
    cmd_status "${1:?Usage: lobsterlan.sh status <peer>}"
    ;;
  peers)
    cmd_peers
    ;;
  help|--help|-h)
    echo "🦞 LobsterLAN — Agent-to-agent communication"
    echo ""
    echo "Commands:"
    echo "  ask <peer> <message>        Synchronous ask (chat completions)"
    echo "  delegate <peer> <message>   Async task delegation (webhooks)"
    echo "  status <peer>               Check peer reachability"
    echo "  peers                       List configured peers"
    echo ""
    echo "Config: \$LOBSTERLAN_CONFIG or ./config/peers.json"
    ;;
  *)
    echo "Unknown command: $1 (try 'help')" >&2
    exit 1
    ;;
esac


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)