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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,7 @@ terraform.tfvars
test/e2e/.dev.vars

# Temporary e2e wrangler configs
.wrangler-e2e-*.jsonc
.wrangler-e2e-*.jsonc

# Local gateway token helper file
.gateway-token.txt
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 28 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ function transformErrorMessage(message: string, host: string): string {
return message;
}

/**
* Build a request for the internal Moltbot gateway.
* If a gateway token is configured, always inject it server-side so users
* don't need to carry `?token=` in browser URLs.
*/
function buildGatewayRequest(request: Request, env: MoltbotEnv): Request {
if (!env.MOLTBOT_GATEWAY_TOKEN) {
return request;
}

const url = new URL(request.url);
url.searchParams.set('token', env.MOLTBOT_GATEWAY_TOKEN);
return new Request(url.toString(), request);
}

export { Sandbox };

/**
Expand Down Expand Up @@ -214,6 +229,11 @@ app.use('/debug/*', async (c, next) => {
});
app.route('/debug', debug);

// Convenience entrypoint for authenticated users: redirects to the gateway UI.
// Token is injected server-side by buildGatewayRequest() in the proxy handler.
app.get('/gateway', (c) => c.redirect('/', 302));
app.get('/gateway/', (c) => c.redirect('/', 302));

// =============================================================================
// CATCH-ALL: Proxy to Moltbot gateway
// =============================================================================
Expand All @@ -222,6 +242,8 @@ app.all('*', async (c) => {
const sandbox = c.get('sandbox');
const request = c.req.raw;
const url = new URL(request.url);
const gatewayRequest = buildGatewayRequest(request, c.env);
const gatewayUrl = new URL(gatewayRequest.url);

console.log('[PROXY] Handling request:', url.pathname);

Expand Down Expand Up @@ -271,15 +293,15 @@ app.all('*', async (c) => {
// Proxy to Moltbot with WebSocket message interception
if (isWebSocketRequest) {
const debugLogs = c.env.DEBUG_ROUTES === 'true';
const redactedSearch = redactSensitiveParams(url);
const redactedSearch = redactSensitiveParams(gatewayUrl);

console.log('[WS] Proxying WebSocket connection to Moltbot');
if (debugLogs) {
console.log('[WS] URL:', url.pathname + redactedSearch);
console.log('[WS] URL:', gatewayUrl.pathname + redactedSearch);
}

// Get WebSocket connection to the container
const containerResponse = await sandbox.wsConnect(request, MOLTBOT_PORT);
const containerResponse = await sandbox.wsConnect(gatewayRequest, MOLTBOT_PORT);
console.log('[WS] wsConnect response status:', containerResponse.status);

// Get the container-side WebSocket
Expand Down Expand Up @@ -399,8 +421,9 @@ app.all('*', async (c) => {
});
}

console.log('[HTTP] Proxying:', url.pathname + url.search);
const httpResponse = await sandbox.containerFetch(request, MOLTBOT_PORT);
const redactedSearch = redactSensitiveParams(gatewayUrl);
console.log('[HTTP] Proxying:', gatewayUrl.pathname + redactedSearch);
const httpResponse = await sandbox.containerFetch(gatewayRequest, MOLTBOT_PORT);
console.log('[HTTP] Response status:', httpResponse.status);

// Add debug header to verify worker handled the request
Expand Down
17 changes: 14 additions & 3 deletions start-moltbot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,21 @@ should_restore_from_r2() {
fi
}

# Restore helper for R2-mounted filesystem:
# avoid preserving ownership/permissions from fuse.s3fs mounts, which can fail
# with "Operation not permitted" when using cp -a.
restore_dir() {
local SRC="$1"
local DEST="$2"

mkdir -p "$DEST"
rsync -rlt --no-perms --no-owner --no-group "$SRC"/ "$DEST"/
}

if [ -f "$BACKUP_DIR/clawdbot/clawdbot.json" ]; then
if should_restore_from_r2; then
echo "Restoring from R2 backup at $BACKUP_DIR/clawdbot..."
cp -a "$BACKUP_DIR/clawdbot/." "$CONFIG_DIR/"
restore_dir "$BACKUP_DIR/clawdbot" "$CONFIG_DIR"
# Copy the sync timestamp to local so we know what version we have
cp -f "$BACKUP_DIR/.last-sync" "$CONFIG_DIR/.last-sync" 2>/dev/null || true
echo "Restored config from R2 backup"
Expand All @@ -84,7 +95,7 @@ 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..."
cp -a "$BACKUP_DIR/." "$CONFIG_DIR/"
restore_dir "$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"
fi
Expand All @@ -100,7 +111,7 @@ if [ -d "$BACKUP_DIR/skills" ] && [ "$(ls -A $BACKUP_DIR/skills 2>/dev/null)" ];
if should_restore_from_r2; then
echo "Restoring skills from $BACKUP_DIR/skills..."
mkdir -p "$SKILLS_DIR"
cp -a "$BACKUP_DIR/skills/." "$SKILLS_DIR/"
restore_dir "$BACKUP_DIR/skills" "$SKILLS_DIR"
echo "Restored skills from R2 backup"
fi
fi
Expand Down
186 changes: 98 additions & 88 deletions wrangler.jsonc
Original file line number Diff line number Diff line change
@@ -1,89 +1,99 @@
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "moltbot-sandbox",
"main": "src/index.ts",
"compatibility_date": "2025-05-06",
"compatibility_flags": ["nodejs_compat"],
"observability": {
"enabled": true,
},
// Static assets for admin UI (built by vite)
"assets": {
"directory": "./dist/client",
"not_found_handling": "single-page-application",
"html_handling": "auto-trailing-slash",
"binding": "ASSETS",
"run_worker_first": true,
},
// Allow importing HTML files as text modules and PNG files as binary
"rules": [
{
"type": "Text",
"globs": ["**/*.html"],
"fallthrough": false,
},
{
"type": "Data",
"globs": ["**/*.png"],
"fallthrough": false,
},
],
// Build command for vite
"build": {
"command": "npm run build",
},
// Container configuration for the Moltbot sandbox
"containers": [
{
"class_name": "Sandbox",
"image": "./Dockerfile",
"instance_type": "standard-4",
"max_instances": 1,
},
],
"durable_objects": {
"bindings": [
{
"class_name": "Sandbox",
"name": "Sandbox",
},
],
},
"migrations": [
{
"new_sqlite_classes": ["Sandbox"],
"tag": "v1",
},
],
// R2 bucket for persistent storage (moltbot data, conversations, etc.)
"r2_buckets": [
{
"binding": "MOLTBOT_BUCKET",
"bucket_name": "moltbot-data",
},
],
// Cron trigger to sync moltbot data to R2 every 5 minutes
"triggers": {
"crons": ["*/5 * * * *"],
},
// Browser Rendering binding for CDP shim
"browser": {
"binding": "BROWSER",
},
// Note: CF_ACCOUNT_ID should be set via `wrangler secret put CF_ACCOUNT_ID`
// Secrets to configure via `wrangler secret put`:
// - ANTHROPIC_API_KEY: Your Anthropic API key
// - CF_ACCESS_TEAM_DOMAIN: Cloudflare Access team domain
// - CF_ACCESS_AUD: Cloudflare Access application audience
// - TELEGRAM_BOT_TOKEN: (optional) Telegram bot token
// - DISCORD_BOT_TOKEN: (optional) Discord bot token
// - SLACK_BOT_TOKEN: (optional) Slack bot token
// - SLACK_APP_TOKEN: (optional) Slack app token
// - MOLTBOT_GATEWAY_TOKEN: (optional) Token to protect gateway access, if unset device pairing will be used
// - CDP_SECRET: (optional) Shared secret for /cdp endpoint authentication
//
// R2 persistent storage secrets (required for data persistence across sessions):
// - R2_ACCESS_KEY_ID: R2 access key ID (from R2 API tokens)
// - R2_SECRET_ACCESS_KEY: R2 secret access key (from R2 API tokens)
// - CF_ACCOUNT_ID: Your Cloudflare account ID (for R2 endpoint URL)
}
"$schema": "node_modules/wrangler/config-schema.json",
"name": "moltbot-sandbox",
"main": "src/index.ts",
"compatibility_date": "2025-05-06",
"compatibility_flags": [
"nodejs_compat"
],
"observability": {
"enabled": true,
},
// Static assets for admin UI (built by vite)
"assets": {
"directory": "./dist/client",
"not_found_handling": "single-page-application",
"html_handling": "auto-trailing-slash",
"binding": "ASSETS",
"run_worker_first": true,
},
// Allow importing HTML files as text modules and PNG files as binary
"rules": [
{
"type": "Text",
"globs": [
"**/*.html"
],
"fallthrough": false,
},
{
"type": "Data",
"globs": [
"**/*.png"
],
"fallthrough": false,
},
],
// Build command for vite
"build": {
"command": "npm run build",
},
// Container configuration for the Moltbot sandbox
"containers": [
{
"class_name": "Sandbox",
"image": "./Dockerfile",
"instance_type": "standard-4",
"max_instances": 1,
},
],
"durable_objects": {
"bindings": [
{
"class_name": "Sandbox",
"name": "Sandbox",
},
],
},
"migrations": [
{
"new_sqlite_classes": [
"Sandbox"
],
"tag": "v1",
},
],
// R2 bucket for persistent storage (moltbot data, conversations, etc.)
"r2_buckets": [
{
"binding": "MOLTBOT_BUCKET",
"bucket_name": "moltbot-data",
},
],
// Cron trigger to sync moltbot data to R2 every 5 minutes
"triggers": {
"crons": [
"*/5 * * * *"
],
},
// Browser Rendering binding for CDP shim
"browser": {
"binding": "BROWSER",
},
// Note: CF_ACCOUNT_ID should be set via `wrangler secret put CF_ACCOUNT_ID`
// Secrets to configure via `wrangler secret put`:
// - ANTHROPIC_API_KEY: Your Anthropic API key
// - CF_ACCESS_TEAM_DOMAIN: Cloudflare Access team domain
// - CF_ACCESS_AUD: Cloudflare Access application audience
// - TELEGRAM_BOT_TOKEN: (optional) Telegram bot token
// - DISCORD_BOT_TOKEN: (optional) Discord bot token
// - SLACK_BOT_TOKEN: (optional) Slack bot token
// - SLACK_APP_TOKEN: (optional) Slack app token
// - MOLTBOT_GATEWAY_TOKEN: (optional) Token to protect gateway access, if unset device pairing will be used
// - CDP_SECRET: (optional) Shared secret for /cdp endpoint authentication
//
// R2 persistent storage secrets (required for data persistence across sessions):
// - R2_ACCESS_KEY_ID: R2 access key ID (from R2 API tokens)
// - R2_SECRET_ACCESS_KEY: R2 secret access key (from R2 API tokens)
// - CF_ACCOUNT_ID: Your Cloudflare account ID (for R2 endpoint URL)
}