-
Notifications
You must be signed in to change notification settings - Fork 22
fix: Resolve ClawHub suspicious flag by making service opt-in #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -12,7 +12,7 @@ | |||||
| <runtime> | ||||||
| <type>node</type> | ||||||
| <entrypoint>index.js</entrypoint> | ||||||
| <persistence>Registers a user-level systemd service (clawbridge.service) that auto-starts on login and restarts on failure.</persistence> | ||||||
| <persistence>Optional. Run setup.sh --enable-service to register a user-level systemd service for auto-start. Not enabled by default.</persistence> | ||||||
| </runtime> | ||||||
|
|
||||||
| <!-- System requirements --> | ||||||
|
|
@@ -49,24 +49,30 @@ | |||||
| <path type="write" location="skills/clawbridge/cloudflared" description="cloudflared binary, only if downloaded during tunnel setup." /> | ||||||
| </filesystem> | ||||||
|
|
||||||
| <!-- Installation — uses the script bundled in this repository --> | ||||||
| <!-- Installation --> | ||||||
| <install> | ||||||
| curl -sL https://raw.githubusercontent.com/dreamwing/clawbridge/master/install.sh | bash | ||||||
| git clone https://github.com/dreamwing/clawbridge.git skills/clawbridge && cd skills/clawbridge && npm install --production && node index.js | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new
Suggested change
|
||||||
| </install> | ||||||
|
|
||||||
| <instructions> | ||||||
| ClawBridge installs itself as a persistent background service. | ||||||
| ClawBridge runs as a foreground Node.js process by default. | ||||||
|
|
||||||
| After installation, the dashboard is accessible at the local IP shown in the terminal output. | ||||||
| An ACCESS_KEY is generated and displayed — keep it safe, it is required to log in. | ||||||
|
|
||||||
| To enable remote access (optional), supply a Cloudflare Tunnel token when prompted, | ||||||
| or leave it blank to use a temporary Quick Tunnel URL. | ||||||
|
|
||||||
| To update to the latest version: | ||||||
| To enable auto-start as a background service (optional): | ||||||
| ./setup.sh --enable-service | ||||||
|
|
||||||
| To install with one command (includes service registration): | ||||||
| curl -sL https://raw.githubusercontent.com/dreamwing/clawbridge/master/install.sh | bash | ||||||
|
|
||||||
| To stop the service: | ||||||
| To update to the latest version via git: | ||||||
| git pull && npm install | ||||||
|
|
||||||
| To stop the service (if enabled): | ||||||
| systemctl --user stop clawbridge | ||||||
|
|
||||||
| Full documentation: https://github.com/dreamwing/clawbridge/blob/master/README.md | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,8 @@ NO_TUNNEL=false | |
| QUICK_TUNNEL=false | ||
| FORCE_CF=false | ||
|
|
||
| ENABLE_SERVICE=false | ||
|
|
||
| for arg in "$@" | ||
| do | ||
| case $arg in | ||
|
|
@@ -42,6 +44,10 @@ do | |
| FORCE_CF=true | ||
| shift | ||
| ;; | ||
| --enable-service) | ||
| ENABLE_SERVICE=true | ||
| shift | ||
| ;; | ||
| esac | ||
| done | ||
|
|
||
|
|
@@ -106,6 +112,7 @@ if [ ! -f "$ENV_FILE" ]; then | |
| RAND_KEY=$(openssl rand -hex 16) | ||
| echo "ACCESS_KEY=$RAND_KEY" > "$ENV_FILE" | ||
| echo "PORT=$PORT" >> "$ENV_FILE" | ||
| chmod 600 "$ENV_FILE" | ||
| echo -e "${YELLOW}🔑 Generated Access Key: $RAND_KEY${NC}" | ||
| else | ||
| echo "✅ Updating .env configuration..." | ||
|
|
@@ -123,6 +130,7 @@ else | |
| else | ||
| RAND_KEY=$ACCESS_KEY | ||
| fi | ||
| chmod 600 "$ENV_FILE" | ||
| fi | ||
|
|
||
| # 3b. Auto-detect OPENCLAW_PATH | ||
|
|
@@ -147,13 +155,14 @@ fi | |
| # 4. Setup Service | ||
| NODE_PATH=$(which node) | ||
|
|
||
| if [ "$OS_TYPE" = "Darwin" ]; then | ||
| # macOS launchd setup | ||
| SERVICE_DIR="$HOME/Library/LaunchAgents" | ||
| mkdir -p "$SERVICE_DIR" | ||
| SERVICE_FILE="$SERVICE_DIR/com.dreamwing.${SERVICE_NAME}.plist" | ||
|
|
||
| cat > "$SERVICE_FILE" <<EOF | ||
| if [ "$ENABLE_SERVICE" = true ]; then | ||
| if [ "$OS_TYPE" = "Darwin" ]; then | ||
|
Comment on lines
+158
to
+159
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This new opt-in gate allows Useful? React with 👍 / 👎. |
||
| # macOS launchd setup | ||
| SERVICE_DIR="$HOME/Library/LaunchAgents" | ||
| mkdir -p "$SERVICE_DIR" | ||
| SERVICE_FILE="$SERVICE_DIR/com.dreamwing.${SERVICE_NAME}.plist" | ||
|
|
||
| cat > "$SERVICE_FILE" <<EOF | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| <plist version="1.0"> | ||
|
|
@@ -183,32 +192,32 @@ if [ "$OS_TYPE" = "Darwin" ]; then | |
| </dict> | ||
| </plist> | ||
| EOF | ||
|
|
||
| echo "📝 Service file created at: $SERVICE_FILE" | ||
| echo "🚀 Loading macOS Launch Agent (com.dreamwing.${SERVICE_NAME})..." | ||
| launchctl load -w "$SERVICE_FILE" >/dev/null 2>&1 || true | ||
| # If already loaded and we just want to restart: | ||
| launchctl unload "$SERVICE_FILE" >/dev/null 2>&1 || true | ||
| launchctl load -w "$SERVICE_FILE" | ||
| echo -e "${GREEN}✅ Service started!${NC}" | ||
| echo "📝 Service file created at: $SERVICE_FILE" | ||
| echo "🚀 Loading macOS Launch Agent (com.dreamwing.${SERVICE_NAME})..." | ||
| launchctl load -w "$SERVICE_FILE" >/dev/null 2>&1 || true | ||
| # If already loaded and we just want to restart: | ||
| launchctl unload "$SERVICE_FILE" >/dev/null 2>&1 || true | ||
| launchctl load -w "$SERVICE_FILE" | ||
| echo -e "${GREEN}✅ Service started!${NC}" | ||
|
|
||
| else | ||
| # Linux systemd setup | ||
| SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service" | ||
| USE_USER_SYSTEMD=true | ||
| else | ||
| # Linux systemd setup | ||
| SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service" | ||
| USE_USER_SYSTEMD=true | ||
|
|
||
| if [ ! -d "$HOME/.config/systemd/user" ]; then | ||
| mkdir -p "$HOME/.config/systemd/user" | ||
| fi | ||
| if [ ! -d "$HOME/.config/systemd/user" ]; then | ||
| mkdir -p "$HOME/.config/systemd/user" | ||
| fi | ||
|
|
||
| # Check if user dbus is active (common issue in bare VPS) | ||
| if ! systemctl --user list-units >/dev/null 2>&1; then | ||
| echo -e "${YELLOW}⚠️ User-level systemd not available. Generating standard systemd file...${NC}" | ||
| USE_USER_SYSTEMD=false | ||
| SERVICE_FILE="/tmp/${SERVICE_NAME}.service" | ||
| fi | ||
| # Check if user dbus is active (common issue in bare VPS) | ||
| if ! systemctl --user list-units >/dev/null 2>&1; then | ||
| echo -e "${YELLOW}⚠️ User-level systemd not available. Generating standard systemd file...${NC}" | ||
| USE_USER_SYSTEMD=false | ||
| SERVICE_FILE="/tmp/${SERVICE_NAME}.service" | ||
| fi | ||
|
|
||
| cat > "$SERVICE_FILE" <<EOF | ||
| cat > "$SERVICE_FILE" <<EOF | ||
| [Unit] | ||
| Description=ClawBridge Dashboard (${SERVICE_NAME}) | ||
| After=network.target | ||
|
|
@@ -226,21 +235,24 @@ EnvironmentFile=$APP_DIR/.env | |
| WantedBy=default.target | ||
| EOF | ||
|
|
||
| echo "📝 Service file created at: $SERVICE_FILE" | ||
|
|
||
| if [ "$USE_USER_SYSTEMD" = true ]; then | ||
| echo "🚀 Enabling User Service ($SERVICE_NAME)..." | ||
| systemctl --user daemon-reload | ||
| systemctl --user enable "$SERVICE_NAME" | ||
| systemctl --user restart "$SERVICE_NAME" | ||
| echo -e "${GREEN}✅ Service started!${NC}" | ||
| else | ||
| echo -e "${YELLOW}👉 Please run the following command with sudo to install the service:${NC}" | ||
| echo "sudo mv $SERVICE_FILE /etc/systemd/system/${SERVICE_NAME}.service" | ||
| echo "sudo systemctl daemon-reload" | ||
| echo "sudo systemctl enable ${SERVICE_NAME}" | ||
| echo "sudo systemctl start ${SERVICE_NAME}" | ||
| echo "📝 Service file created at: $SERVICE_FILE" | ||
|
|
||
| if [ "$USE_USER_SYSTEMD" = true ]; then | ||
| echo "🚀 Enabling User Service ($SERVICE_NAME)..." | ||
| systemctl --user daemon-reload | ||
| systemctl --user enable "$SERVICE_NAME" | ||
| systemctl --user restart "$SERVICE_NAME" | ||
| echo -e "${GREEN}✅ Service started!${NC}" | ||
| else | ||
| echo -e "${YELLOW}👉 Please run the following command with sudo to install the service:${NC}" | ||
| echo "sudo mv $SERVICE_FILE /etc/systemd/system/${SERVICE_NAME}.service" | ||
| echo "sudo systemctl daemon-reload" | ||
| echo "sudo systemctl enable ${SERVICE_NAME}" | ||
| echo "sudo systemctl start ${SERVICE_NAME}" | ||
| fi | ||
| fi | ||
| else | ||
| echo -e "${YELLOW}ℹ️ Skipping service registration. To enable auto-start, run with: ./setup.sh --enable-service${NC}" | ||
| fi | ||
|
|
||
| # 5. Remote Access (Cloudflare Tunnel) | ||
|
|
@@ -319,32 +331,17 @@ if [[ "$ENABLE_TUNNEL" =~ ^[Yy]$ ]] || [ "$USE_VPN" = true ]; then | |
| ENABLE_TUNNEL="y" | ||
| fi | ||
|
|
||
| if ! command -v cloudflared &> /dev/null; then | ||
| echo "⬇️ Downloading cloudflared..." | ||
| # Detect arch | ||
| ARCH=$(uname -m) | ||
| if ! command -v cloudflared &> /dev/null && [ ! -x "./cloudflared" ]; then | ||
| echo -e "${YELLOW}⚠️ cloudflared not found.${NC}" | ||
| echo " Please install it manually to enable remote access:" | ||
| if [ "$OS_TYPE" = "Darwin" ]; then | ||
| if [[ "$ARCH" == "x86_64" ]] || [[ "$ARCH" == "amd64" ]]; then | ||
| wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz -O cloudflared.tgz | ||
| tar -xzf cloudflared.tgz && rm cloudflared.tgz | ||
| elif [[ "$ARCH" == "arm64" ]] || [[ "$ARCH" == "aarch64" ]]; then | ||
| wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz -O cloudflared.tgz | ||
| tar -xzf cloudflared.tgz && rm cloudflared.tgz | ||
| else | ||
| echo "❌ Architecture $ARCH not supported for macOS auto-download." | ||
| exit 1 | ||
| fi | ||
| echo " brew install cloudflared" | ||
| else | ||
| if [[ "$ARCH" == "x86_64" ]]; then | ||
| wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -O cloudflared | ||
| elif [[ "$ARCH" == "aarch64" ]]; then | ||
| wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64 -O cloudflared | ||
| else | ||
| echo "❌ Architecture $ARCH not supported for Linux auto-download." | ||
| exit 1 | ||
| fi | ||
| echo " sudo apt install cloudflared (Debian/Ubuntu)" | ||
| echo " Or: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" | ||
| fi | ||
| chmod +x cloudflared | ||
| echo " Then re-run this setup." | ||
| ENABLE_TUNNEL="n" | ||
| fi | ||
|
Comment on lines
+344
to
345
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When cloudflared is not installed, this branch sets Useful? React with 👍 / 👎. |
||
|
|
||
| # If no token and NOT quick mode, ask for it | ||
|
|
@@ -465,30 +462,34 @@ fi | |
| if [ "$QUICK_TUNNEL" = true ] || [ -z "$CF_TOKEN" ]; then | ||
| # ONLY if VPN is NOT used OR Force CF is enabled | ||
| if [ "$USE_VPN" = false ] || [ "$FORCE_CF" = true ]; then | ||
| echo "⏳ Waiting for Quick Tunnel URL (max 20s)..." | ||
|
|
||
| # Loop wait for 20s | ||
| for i in {1..20}; do | ||
| if [ -f "$APP_DIR/.quick_tunnel_url" ]; then | ||
| QURL=$(cat "$APP_DIR/.quick_tunnel_url") | ||
| echo -e "\n${GREEN}🚀 ClawBridge Dashboard Live:${NC}" | ||
| echo -e "👉 ${BLUE}${QURL}${NC}" | ||
| echo -e "⚠️ Note: This link expires if the dashboard restarts." | ||
| print_qr "$QURL" | ||
| break | ||
| fi | ||
| sleep 1 | ||
| echo -n "." | ||
| done | ||
|
|
||
| if [ ! -f "$APP_DIR/.quick_tunnel_url" ]; then | ||
| if [ "$OS_TYPE" = "Darwin" ]; then | ||
| echo -e "\n${YELLOW}⚠️ URL not ready yet. Check logs later: tail -f /tmp/com.dreamwing.${SERVICE_NAME}.log${NC}" | ||
| elif [ "$USE_USER_SYSTEMD" = true ]; then | ||
| echo -e "\n${YELLOW}⚠️ URL not ready yet. Check logs later: journalctl --user -u ${SERVICE_NAME} -f${NC}" | ||
| else | ||
| echo -e "\n${YELLOW}⚠️ URL not ready yet. Check logs later: sudo journalctl -u ${SERVICE_NAME} -f${NC}" | ||
| if [ "$ENABLE_SERVICE" = true ]; then | ||
| echo "⏳ Waiting for Quick Tunnel URL (max 20s)..." | ||
|
|
||
| # Loop wait for 20s | ||
| for i in {1..20}; do | ||
| if [ -f "$APP_DIR/.quick_tunnel_url" ]; then | ||
| QURL=$(cat "$APP_DIR/.quick_tunnel_url") | ||
| echo -e "\n${GREEN}🚀 ClawBridge Dashboard Live:${NC}" | ||
| echo -e "👉 ${BLUE}${QURL}${NC}" | ||
| echo -e "⚠️ Note: This link expires if the dashboard restarts." | ||
| print_qr "$QURL" | ||
| break | ||
| fi | ||
| sleep 1 | ||
| echo -n "." | ||
| done | ||
|
|
||
| if [ ! -f "$APP_DIR/.quick_tunnel_url" ]; then | ||
| if [ "$OS_TYPE" = "Darwin" ]; then | ||
| echo -e "\n${YELLOW}⚠️ URL not ready yet. Check logs later: tail -f /tmp/com.dreamwing.${SERVICE_NAME}.log${NC}" | ||
| elif [ "$USE_USER_SYSTEMD" = true ]; then | ||
| echo -e "\n${YELLOW}⚠️ URL not ready yet. Check logs later: journalctl --user -u ${SERVICE_NAME} -f${NC}" | ||
| else | ||
| echo -e "\n${YELLOW}⚠️ URL not ready yet. Check logs later: sudo journalctl -u ${SERVICE_NAME} -f${NC}" | ||
| fi | ||
| fi | ||
| else | ||
| echo -e "\n${YELLOW}ℹ️ Quick Tunnel is configured. Run 'npm start' or 'node index.js' to see your public URL.${NC}" | ||
| fi | ||
| fi | ||
| fi | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,70 +4,25 @@ const os = require('os'); | |
| const { spawn } = require('child_process'); | ||
|
|
||
| const BIN_NAME = 'cloudflared'; | ||
| const BIN_PATH = path.join(__dirname, BIN_NAME); | ||
| let BIN_PATH = path.join(__dirname, BIN_NAME); | ||
| const PID_FILE = path.join(__dirname, '.cloudflared.pid'); | ||
|
|
||
| function getDownloadUrl() { | ||
| const arch = os.arch(); // 'x64', 'arm64', etc. | ||
| const platform = os.platform(); // 'linux', 'darwin', etc. | ||
|
|
||
| const archMap = { | ||
| 'x64': 'amd64', | ||
| 'arm64': 'arm64', | ||
| 'arm': 'arm', | ||
| }; | ||
|
|
||
| const cfArch = archMap[arch]; | ||
| if (!cfArch) { | ||
| throw new Error(`Unsupported architecture: ${arch}. Supported: x64, arm64, arm`); | ||
| } | ||
|
|
||
| if (platform === 'linux') { | ||
| return `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}`; | ||
| } else if (platform === 'darwin') { | ||
| return `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${cfArch}.tgz`; | ||
| } | ||
|
|
||
| throw new Error(`Unsupported platform: ${platform}. Supported: linux, darwin`); | ||
| } | ||
|
|
||
| async function downloadBinary() { | ||
| // Check local binary first (legacy or manual placement) | ||
| if (fs.existsSync(BIN_PATH) && fs.statSync(BIN_PATH).size > 1000000) return; | ||
|
|
||
| const url = getDownloadUrl(); | ||
| console.log(`[Tunnel] Downloading cloudflared for ${os.platform()}/${os.arch()}...`); | ||
|
|
||
| const https = require('https'); | ||
| const http = require('http'); | ||
| // Check system PATH | ||
| const { execSync } = require('child_process'); | ||
| try { | ||
| const systemPath = execSync('which cloudflared', { encoding: 'utf8' }).trim(); | ||
| if (systemPath && fs.existsSync(systemPath)) { | ||
| BIN_PATH = systemPath; | ||
| return; | ||
| } | ||
| } catch (e) { /* expected if not found in PATH */ } | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| const download = (downloadUrl, redirects = 0) => { | ||
| if (redirects > 5) return reject(new Error('Too many redirects')); | ||
| const client = downloadUrl.startsWith('https') ? https : http; | ||
| client.get(downloadUrl, (res) => { | ||
| // Follow redirects (GitHub releases use 302) | ||
| if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { | ||
| return download(res.headers.location, redirects + 1); | ||
| } | ||
| if (res.statusCode !== 200) { | ||
| return reject(new Error(`Download failed: HTTP ${res.statusCode}`)); | ||
| } | ||
| const file = fs.createWriteStream(BIN_PATH); | ||
| res.pipe(file); | ||
| file.on('finish', () => { | ||
| file.close(); | ||
| fs.chmodSync(BIN_PATH, '755'); | ||
| console.log('[Tunnel] Download complete.'); | ||
| resolve(); | ||
| }); | ||
| file.on('error', (err) => { | ||
| fs.unlinkSync(BIN_PATH); | ||
| reject(err); | ||
| }); | ||
| }).on('error', reject); | ||
| }; | ||
| download(url); | ||
| }); | ||
| // If both fail, throw error so index.js catches it and doesn't start tunnel | ||
| throw new Error('cloudflared not found. Install via: brew install cloudflared (macOS) or apt install cloudflared (Linux)'); | ||
| } | ||
|
Comment on lines
10
to
26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The function was renamed in spirit but not in name: it now only resolves an existing binary (checks local path, then falls back to Consider renaming it to |
||
|
|
||
| function stopExistingTunnel() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
<install>command startsnode index.jsdirectly, but fresh installs do not have.envyet andsrc/config.jsexits whenACCESS_KEYis unset. That means the documented install path fails to boot and does not generate credentials for login. Call./setup.sh(or otherwise create.envwithACCESS_KEY) before starting the Node process.Useful? React with 👍 / 👎.