diff --git a/.gitignore b/.gitignore index fad199338..c2e0ac68b 100644 --- a/.gitignore +++ b/.gitignore @@ -57,4 +57,7 @@ terraform.tfvars test/e2e/.dev.vars # Temporary e2e wrangler configs -.wrangler-e2e-*.jsonc \ No newline at end of file +.wrangler-e2e-*.jsonc + +# Local gateway token helper file +.gateway-token.txt diff --git a/package-lock.json b/package-lock.json index 170a6f261..a4082ec6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "moltbot-sandbox", "version": "1.0.0", - "license": "MIT", + "license": "Apache-2.0", "dependencies": { "@cloudflare/puppeteer": "^1.0.5", "hono": "^4.11.6", diff --git a/src/index.ts b/src/index.ts index ed08910cf..7e068e457 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 }; /** @@ -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 // ============================================================================= @@ -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); @@ -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 @@ -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 diff --git a/start-moltbot.sh b/start-moltbot.sh index 286a4d67f..a2df9a069 100644 --- a/start-moltbot.sh +++ b/start-moltbot.sh @@ -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" @@ -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 @@ -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 diff --git a/wrangler.jsonc b/wrangler.jsonc index 7a65d9481..76709b1c6 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -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) +} \ No newline at end of file