Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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/
Expand Down
16 changes: 8 additions & 8 deletions src/gateway/env.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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',
});
});
Expand Down
8 changes: 4 additions & 4 deletions src/gateway/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ export function buildEnvVars(env: MoltbotEnv): Record<string, string> {
} 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;
Expand Down
12 changes: 6 additions & 6 deletions src/gateway/process.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createMockSandbox } from '../test-utils';
function createFullMockProcess(overrides: Partial<Process> = {}): Process {
return {
id: 'test-id',
command: 'clawdbot gateway',
command: 'openclaw gateway',
status: 'running',
startTime: new Date(),
endTime: undefined,
Expand All @@ -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);
Expand All @@ -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();
Expand Down Expand Up @@ -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({
Expand Down
13 changes: 6 additions & 7 deletions src/gateway/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ export async function findExistingMoltbotProcess(sandbox: Sandbox): Promise<Proc
try {
const processes = await sandbox.listProcesses();
for (const proc of processes) {
// Only match the gateway process, not CLI commands like "clawdbot devices list"
// Note: CLI is still named "clawdbot" until upstream renames it
const isGatewayProcess =
// Only match the gateway process, not CLI commands like "openclaw devices list"
const isGatewayProcess =
proc.command.includes('start-moltbot.sh') ||
proc.command.includes('clawdbot gateway');
const isCliCommand =
proc.command.includes('clawdbot devices') ||
proc.command.includes('clawdbot --version');
proc.command.includes('openclaw gateway');
const isCliCommand =
proc.command.includes('openclaw devices') ||
proc.command.includes('openclaw --version');

if (isGatewayProcess && !isCliCommand) {
if (proc.status === 'starting' || proc.status === 'running') {
Expand Down
11 changes: 5 additions & 6 deletions src/gateway/sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,18 @@ describe('syncToR2', () => {
});

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');
});
});
Expand Down Expand Up @@ -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/');
});
});
Expand Down
10 changes: 5 additions & 5 deletions src/gateway/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise<SyncR
// Sanity check: verify source has critical files before syncing
// This prevents accidentally overwriting a good backup with empty/corrupted data
try {
const checkProc = await sandbox.startProcess('test -f /root/.clawdbot/clawdbot.json && echo "ok"');
const checkProc = await sandbox.startProcess('test -f /root/.openclaw/openclaw.json && echo "ok"');
await waitForProcess(checkProc, 5000);
const checkLogs = await checkProc.getLogs();
if (!checkLogs.stdout?.includes('ok')) {
return {
success: false,
error: 'Sync aborted: source missing clawdbot.json',
return {
success: false,
error: 'Sync aborted: source missing openclaw.json',
details: 'The local config directory is missing critical files. This could indicate corruption or an incomplete setup.',
};
}
Expand All @@ -59,7 +59,7 @@ export async function syncToR2(sandbox: Sandbox, env: MoltbotEnv): Promise<SyncR

// Run rsync to backup config to R2
// Note: Use --no-times because s3fs doesn't support setting timestamps
const syncCmd = `rsync -r --no-times --delete --exclude='*.lock' --exclude='*.log' --exclude='*.tmp' /root/.clawdbot/ ${R2_MOUNT_PATH}/clawdbot/ && rsync -r --no-times --delete /root/clawd/skills/ ${R2_MOUNT_PATH}/skills/ && date -Iseconds > ${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);
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading