diff --git a/Dockerfile b/Dockerfile index 3fb55a3..65d7a0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM docker.io/cloudflare/sandbox:0.7.0 -# Install Node.js 22 (required by clawdbot) and rsync (for R2 backup sync) +# Install Node.js 22 (required by openclaw) and rsync (for R2 backup sync) # The base image has Node 20, we need to replace it with Node 22 # Using direct binary download for reliability ENV NODE_VERSION=22.13.1 @@ -14,15 +14,15 @@ RUN apt-get update && apt-get install -y xz-utils ca-certificates rsync \ # Install pnpm globally RUN npm install -g pnpm -# Install moltbot (CLI is still named clawdbot until upstream renames) +# Install OpenClaw (gateway CLI) # Pin to specific version for reproducible builds -RUN npm install -g clawdbot@2026.1.24-3 \ - && clawdbot --version +RUN npm install -g openclaw@2026.1.29 \ + && openclaw --version -# Create moltbot directories (paths still use clawdbot until upstream renames) -# Templates are stored in /root/.clawdbot-templates for initialization -RUN mkdir -p /root/.clawdbot \ - && mkdir -p /root/.clawdbot-templates \ +# Create moltbot directories +# Templates are stored in /root/.openclaw-templates for initialization +RUN mkdir -p /root/.openclaw \ + && mkdir -p /root/.openclaw-templates \ && mkdir -p /root/clawd \ && mkdir -p /root/clawd/skills @@ -32,7 +32,7 @@ COPY start-moltbot.sh /usr/local/bin/start-moltbot.sh RUN chmod +x /usr/local/bin/start-moltbot.sh # Copy default configuration template -COPY moltbot.json.template /root/.clawdbot-templates/moltbot.json.template +COPY moltbot.json.template /root/.openclaw-templates/moltbot.json.template # Copy custom skills COPY skills/ /root/clawd/skills/ diff --git a/src/gateway/env.test.ts b/src/gateway/env.test.ts index 29f033d..448dabb 100644 --- a/src/gateway/env.test.ts +++ b/src/gateway/env.test.ts @@ -85,10 +85,10 @@ describe('buildEnvVars', () => { expect(result.OPENAI_API_KEY).toBe('sk-openai-key'); }); - it('maps MOLTBOT_GATEWAY_TOKEN to CLAWDBOT_GATEWAY_TOKEN for container', () => { + it('maps MOLTBOT_GATEWAY_TOKEN to OPENCLAW_GATEWAY_TOKEN for container', () => { const env = createMockEnv({ MOLTBOT_GATEWAY_TOKEN: 'my-token' }); const result = buildEnvVars(env); - expect(result.CLAWDBOT_GATEWAY_TOKEN).toBe('my-token'); + expect(result.OPENCLAW_GATEWAY_TOKEN).toBe('my-token'); }); it('includes all channel tokens when set', () => { @@ -110,15 +110,15 @@ describe('buildEnvVars', () => { expect(result.SLACK_APP_TOKEN).toBe('slack-app'); }); - it('maps DEV_MODE to CLAWDBOT_DEV_MODE for container', () => { + it('maps DEV_MODE to OPENCLAW_DEV_MODE for container', () => { const env = createMockEnv({ DEV_MODE: 'true', - CLAWDBOT_BIND_MODE: 'lan', + OPENCLAW_BIND_MODE: 'lan', }); const result = buildEnvVars(env); - - expect(result.CLAWDBOT_DEV_MODE).toBe('true'); - expect(result.CLAWDBOT_BIND_MODE).toBe('lan'); + + expect(result.OPENCLAW_DEV_MODE).toBe('true'); + expect(result.OPENCLAW_BIND_MODE).toBe('lan'); }); it('combines all env vars correctly', () => { @@ -131,7 +131,7 @@ describe('buildEnvVars', () => { expect(result).toEqual({ ANTHROPIC_API_KEY: 'sk-key', - CLAWDBOT_GATEWAY_TOKEN: 'token', + OPENCLAW_GATEWAY_TOKEN: 'token', TELEGRAM_BOT_TOKEN: 'tg', }); }); diff --git a/src/gateway/env.ts b/src/gateway/env.ts index a57e781..ce0fae2 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -43,10 +43,10 @@ export function buildEnvVars(env: MoltbotEnv): Record { } else if (env.ANTHROPIC_BASE_URL) { envVars.ANTHROPIC_BASE_URL = env.ANTHROPIC_BASE_URL; } - // Map MOLTBOT_GATEWAY_TOKEN to CLAWDBOT_GATEWAY_TOKEN (container expects this name) - if (env.MOLTBOT_GATEWAY_TOKEN) envVars.CLAWDBOT_GATEWAY_TOKEN = env.MOLTBOT_GATEWAY_TOKEN; - if (env.DEV_MODE) envVars.CLAWDBOT_DEV_MODE = env.DEV_MODE; // Pass DEV_MODE as CLAWDBOT_DEV_MODE to container - if (env.CLAWDBOT_BIND_MODE) envVars.CLAWDBOT_BIND_MODE = env.CLAWDBOT_BIND_MODE; + // Map Worker env vars to OPENCLAW_* for container + if (env.MOLTBOT_GATEWAY_TOKEN) envVars.OPENCLAW_GATEWAY_TOKEN = env.MOLTBOT_GATEWAY_TOKEN; + if (env.DEV_MODE) envVars.OPENCLAW_DEV_MODE = env.DEV_MODE; + if (env.OPENCLAW_BIND_MODE) envVars.OPENCLAW_BIND_MODE = env.OPENCLAW_BIND_MODE; if (env.TELEGRAM_BOT_TOKEN) envVars.TELEGRAM_BOT_TOKEN = env.TELEGRAM_BOT_TOKEN; if (env.TELEGRAM_DM_POLICY) envVars.TELEGRAM_DM_POLICY = env.TELEGRAM_DM_POLICY; if (env.DISCORD_BOT_TOKEN) envVars.DISCORD_BOT_TOKEN = env.DISCORD_BOT_TOKEN; diff --git a/src/gateway/process.test.ts b/src/gateway/process.test.ts index 4243658..094683b 100644 --- a/src/gateway/process.test.ts +++ b/src/gateway/process.test.ts @@ -7,7 +7,7 @@ import { createMockSandbox } from '../test-utils'; function createFullMockProcess(overrides: Partial = {}): Process { return { id: 'test-id', - command: 'clawdbot gateway', + command: 'openclaw gateway', status: 'running', startTime: new Date(), endTime: undefined, @@ -28,8 +28,8 @@ describe('findExistingMoltbotProcess', () => { it('returns null when only CLI commands are running', async () => { const processes = [ - createFullMockProcess({ command: 'clawdbot devices list --json', status: 'running' }), - createFullMockProcess({ command: 'clawdbot --version', status: 'completed' }), + createFullMockProcess({ command: 'openclaw devices list --json', status: 'running' }), + createFullMockProcess({ command: 'openclaw --version', status: 'completed' }), ]; const { sandbox, listProcessesMock } = createMockSandbox(); listProcessesMock.mockResolvedValue(processes); @@ -41,11 +41,11 @@ describe('findExistingMoltbotProcess', () => { it('returns gateway process when running', async () => { const gatewayProcess = createFullMockProcess({ id: 'gateway-1', - command: 'clawdbot gateway --port 18789', + command: 'openclaw gateway --port 18789', status: 'running' }); const processes = [ - createFullMockProcess({ command: 'clawdbot devices list', status: 'completed' }), + createFullMockProcess({ command: 'openclaw devices list', status: 'completed' }), gatewayProcess, ]; const { sandbox, listProcessesMock } = createMockSandbox(); @@ -105,7 +105,7 @@ describe('findExistingMoltbotProcess', () => { it('returns first matching gateway process', async () => { const firstGateway = createFullMockProcess({ id: 'gateway-1', - command: 'clawdbot gateway', + command: 'openclaw gateway', status: 'running' }); const secondGateway = createFullMockProcess({ diff --git a/src/gateway/process.ts b/src/gateway/process.ts index aa35e06..b008da2 100644 --- a/src/gateway/process.ts +++ b/src/gateway/process.ts @@ -14,14 +14,13 @@ export async function findExistingMoltbotProcess(sandbox: Sandbox): Promise { }); describe('sanity checks', () => { - it('returns error when source is missing clawdbot.json', async () => { + it('returns error when source is missing openclaw.json', async () => { const { sandbox, startProcessMock } = createMockSandbox(); startProcessMock .mockResolvedValueOnce(createMockProcess('s3fs on /data/moltbot type fuse.s3fs\n')) .mockResolvedValueOnce(createMockProcess('')); // No "ok" output - + const env = createMockEnvWithR2(); const result = await syncToR2(sandbox, env); - // Error message still references clawdbot.json since that's the actual file name expect(result.success).toBe(false); - expect(result.error).toBe('Sync aborted: source missing clawdbot.json'); + expect(result.error).toBe('Sync aborted: source missing openclaw.json'); expect(result.details).toContain('missing critical files'); }); }); @@ -108,12 +107,12 @@ describe('syncToR2', () => { await syncToR2(sandbox, env); - // Third call should be rsync (paths still use clawdbot internally) + // Third call should be rsync const rsyncCall = startProcessMock.mock.calls[2][0]; expect(rsyncCall).toContain('rsync'); expect(rsyncCall).toContain('--no-times'); expect(rsyncCall).toContain('--delete'); - expect(rsyncCall).toContain('/root/.clawdbot/'); + expect(rsyncCall).toContain('/root/.openclaw/'); expect(rsyncCall).toContain('/data/moltbot/'); }); }); diff --git a/src/gateway/sync.ts b/src/gateway/sync.ts index a10c711..4633199 100644 --- a/src/gateway/sync.ts +++ b/src/gateway/sync.ts @@ -39,13 +39,13 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise ${R2_MOUNT_PATH}/.last-sync`; + const syncCmd = `rsync -r --no-times --delete --exclude='*.lock' --exclude='*.log' --exclude='*.tmp' /root/.openclaw/ ${R2_MOUNT_PATH}/openclaw/ && rsync -r --no-times --delete /root/clawd/skills/ ${R2_MOUNT_PATH}/skills/ && date -Iseconds > ${R2_MOUNT_PATH}/.last-sync`; try { const proc = await sandbox.startProcess(syncCmd); diff --git a/src/types.ts b/src/types.ts index bb82c8c..c62bff9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,9 +14,9 @@ export interface MoltbotEnv { ANTHROPIC_API_KEY?: string; ANTHROPIC_BASE_URL?: string; OPENAI_API_KEY?: string; - MOLTBOT_GATEWAY_TOKEN?: string; // Gateway token (mapped to CLAWDBOT_GATEWAY_TOKEN for container) + MOLTBOT_GATEWAY_TOKEN?: string; // Gateway token (mapped to OPENCLAW_GATEWAY_TOKEN for container) - CLAWDBOT_BIND_MODE?: string; + OPENCLAW_BIND_MODE?: string; DEV_MODE?: string; // Set to 'true' for local dev (skips CF Access auth + moltbot device pairing) DEBUG_ROUTES?: string; // Set to 'true' to enable /debug/* routes SANDBOX_SLEEP_AFTER?: string; // How long before sandbox sleeps: 'never' (default), or duration like '10m', '1h' diff --git a/start-moltbot.sh b/start-moltbot.sh index 7e225e8..94ef784 100644 --- a/start-moltbot.sh +++ b/start-moltbot.sh @@ -8,17 +8,16 @@ set -e -# Check if clawdbot gateway is already running - bail early if so -# Note: CLI is still named "clawdbot" until upstream renames it -if pgrep -f "clawdbot gateway" > /dev/null 2>&1; then +# Check if openclaw gateway is already running - bail early if so +if pgrep -f "openclaw gateway" > /dev/null 2>&1; then echo "Moltbot gateway is already running, exiting." exit 0 fi -# Paths (clawdbot paths are used internally - upstream hasn't renamed yet) -CONFIG_DIR="/root/.clawdbot" -CONFIG_FILE="$CONFIG_DIR/clawdbot.json" -TEMPLATE_DIR="/root/.clawdbot-templates" +# Paths +CONFIG_DIR="/root/.openclaw" +CONFIG_FILE="$CONFIG_DIR/openclaw.json" +TEMPLATE_DIR="/root/.openclaw-templates" TEMPLATE_FILE="$TEMPLATE_DIR/moltbot.json.template" BACKUP_DIR="/data/moltbot" @@ -31,38 +30,38 @@ mkdir -p "$CONFIG_DIR" # ============================================================ # RESTORE FROM R2 BACKUP # ============================================================ -# Check if R2 backup exists by looking for clawdbot.json +# Check if R2 backup exists by looking for openclaw.json (or legacy clawdbot.json) # The BACKUP_DIR may exist but be empty if R2 was just mounted -# Note: backup structure is $BACKUP_DIR/clawdbot/ and $BACKUP_DIR/skills/ +# Note: backup structure is $BACKUP_DIR/openclaw/ and $BACKUP_DIR/skills/ # Helper function to check if R2 backup is newer than local should_restore_from_r2() { local R2_SYNC_FILE="$BACKUP_DIR/.last-sync" local LOCAL_SYNC_FILE="$CONFIG_DIR/.last-sync" - + # If no R2 sync timestamp, don't restore if [ ! -f "$R2_SYNC_FILE" ]; then echo "No R2 sync timestamp found, skipping restore" return 1 fi - + # If no local sync timestamp, restore from R2 if [ ! -f "$LOCAL_SYNC_FILE" ]; then echo "No local sync timestamp, will restore from R2" return 0 fi - + # Compare timestamps R2_TIME=$(cat "$R2_SYNC_FILE" 2>/dev/null) LOCAL_TIME=$(cat "$LOCAL_SYNC_FILE" 2>/dev/null) - + echo "R2 last sync: $R2_TIME" echo "Local last sync: $LOCAL_TIME" - + # Convert to epoch seconds for comparison R2_EPOCH=$(date -d "$R2_TIME" +%s 2>/dev/null || echo "0") LOCAL_EPOCH=$(date -d "$LOCAL_TIME" +%s 2>/dev/null || echo "0") - + if [ "$R2_EPOCH" -gt "$LOCAL_EPOCH" ]; then echo "R2 backup is newer, will restore" return 0 @@ -72,21 +71,37 @@ should_restore_from_r2() { fi } -if [ -f "$BACKUP_DIR/clawdbot/clawdbot.json" ]; then +# Try new backup format first, then fall back to legacy +if [ -f "$BACKUP_DIR/openclaw/openclaw.json" ]; then if should_restore_from_r2; then - echo "Restoring from R2 backup at $BACKUP_DIR/clawdbot..." - cp -a "$BACKUP_DIR/clawdbot/." "$CONFIG_DIR/" - # Copy the sync timestamp to local so we know what version we have + echo "Restoring from R2 backup at $BACKUP_DIR/openclaw..." + cp -a "$BACKUP_DIR/openclaw/." "$CONFIG_DIR/" cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true echo "Restored config from R2 backup" fi +elif [ -f "$BACKUP_DIR/clawdbot/clawdbot.json" ]; then + # Legacy backup format (pre-openclaw rename) + if should_restore_from_r2; then + echo "Restoring from legacy R2 backup at $BACKUP_DIR/clawdbot..." + cp -a "$BACKUP_DIR/clawdbot/." "$CONFIG_DIR/" + cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true + # Rename legacy config file to new name + if [ -f "$CONFIG_DIR/clawdbot.json" ] && [ ! -f "$CONFIG_FILE" ]; then + mv "$CONFIG_DIR/clawdbot.json" "$CONFIG_FILE" + fi + echo "Restored config from legacy R2 backup (migrated clawdbot -> openclaw)" + fi elif [ -f "$BACKUP_DIR/clawdbot.json" ]; then # Legacy backup format (flat structure) if should_restore_from_r2; then - echo "Restoring from legacy R2 backup at $BACKUP_DIR..." + echo "Restoring from legacy flat R2 backup at $BACKUP_DIR..." cp -a "$BACKUP_DIR/." "$CONFIG_DIR/" cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true - echo "Restored config from legacy R2 backup" + # Rename legacy config file to new name + if [ -f "$CONFIG_DIR/clawdbot.json" ] && [ ! -f "$CONFIG_FILE" ]; then + mv "$CONFIG_DIR/clawdbot.json" "$CONFIG_FILE" + fi + echo "Restored config from legacy flat R2 backup (migrated clawdbot -> openclaw)" fi elif [ -d "$BACKUP_DIR" ]; then echo "R2 mounted at $BACKUP_DIR but no backup data found yet" @@ -136,7 +151,7 @@ fi node << EOFNODE const fs = require('fs'); -const configPath = '/root/.clawdbot/clawdbot.json'; +const configPath = '/root/.openclaw/openclaw.json'; console.log('Updating config at:', configPath); let config = {}; @@ -171,13 +186,13 @@ config.gateway.mode = 'local'; config.gateway.trustedProxies = ['10.1.0.0']; // Set gateway token if provided -if (process.env.CLAWDBOT_GATEWAY_TOKEN) { +if (process.env.OPENCLAW_GATEWAY_TOKEN) { config.gateway.auth = config.gateway.auth || {}; - config.gateway.auth.token = process.env.CLAWDBOT_GATEWAY_TOKEN; + config.gateway.auth.token = process.env.OPENCLAW_GATEWAY_TOKEN; } // Allow insecure auth for dev mode -if (process.env.CLAWDBOT_DEV_MODE === 'true') { +if (process.env.OPENCLAW_DEV_MODE === 'true') { config.gateway.controlUi = config.gateway.controlUi || {}; config.gateway.controlUi.allowInsecureAuth = true; } @@ -279,16 +294,19 @@ echo "Starting Moltbot Gateway..." echo "Gateway will be available on port 18789" # Clean up stale lock files -rm -f /tmp/clawdbot-gateway.lock 2>/dev/null || true +rm -f /tmp/openclaw-gateway.lock 2>/dev/null || true rm -f "$CONFIG_DIR/gateway.lock" 2>/dev/null || true BIND_MODE="lan" -echo "Dev mode: ${CLAWDBOT_DEV_MODE:-false}, Bind mode: $BIND_MODE" +echo "Dev mode: ${OPENCLAW_DEV_MODE:-false}, Bind mode: $BIND_MODE" -if [ -n "$CLAWDBOT_GATEWAY_TOKEN" ]; then - echo "Starting gateway with token auth..." - exec clawdbot gateway --port 18789 --verbose --allow-unconfigured --bind "$BIND_MODE" --token "$CLAWDBOT_GATEWAY_TOKEN" -else - echo "Starting gateway with device pairing (no token)..." - exec clawdbot gateway --port 18789 --verbose --allow-unconfigured --bind "$BIND_MODE" +# OpenClaw v2026.1.29+ requires auth for non-loopback binds. +# Generate a random token if none was provided, since the Worker handles +# its own auth layer and always communicates over localhost. +if [ -z "$OPENCLAW_GATEWAY_TOKEN" ]; then + OPENCLAW_GATEWAY_TOKEN=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 32) + echo "No gateway token provided, generated random token for internal use" fi + +echo "Starting gateway with token auth..." +exec openclaw gateway --port 18789 --verbose --allow-unconfigured --bind "$BIND_MODE" --token "$OPENCLAW_GATEWAY_TOKEN"