From fd9fd0094c216d171167d00ba916177a09a41500 Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 16:20:32 +0100 Subject: [PATCH 1/8] docs(readme): update for native Cloudflare AI Gateway support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Quick Start: replace legacy AI_GATEWAY_* with new env vars - AI Gateway section: rewrite with explanation of how the gateway proxies requests and what each secret actually is - Clarify that CLOUDFLARE_AI_GATEWAY_API_KEY is your provider's API key (e.g. Anthropic key), not a Cloudflare-specific key — it's forwarded through the gateway to the upstream provider - All three secrets (API key + account ID + gateway ID) are required - Secrets table: add new vars, deprecate old AI_GATEWAY_* vars - Add legacy deprecation note --- README.md | 50 +++++++++++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index a26a67ee..73c9389f 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,10 @@ npm install # Set your API key (direct Anthropic access) npx wrangler secret put ANTHROPIC_API_KEY -# Or use AI Gateway instead (see "Optional: Cloudflare AI Gateway" below) -# npx wrangler secret put AI_GATEWAY_API_KEY -# npx wrangler secret put AI_GATEWAY_BASE_URL +# Or use Cloudflare AI Gateway instead (see "Optional: Cloudflare AI Gateway" below) +# npx wrangler secret put CLOUDFLARE_AI_GATEWAY_API_KEY +# npx wrangler secret put CF_AI_GATEWAY_ACCOUNT_ID +# npx wrangler secret put CF_AI_GATEWAY_GATEWAY_ID # Generate and set a gateway token (required for remote access) # Save this token - you'll need it to access the Control UI @@ -348,42 +349,53 @@ See `skills/cloudflare-browser/SKILL.md` for full documentation. ## Optional: Cloudflare AI Gateway -You can route API requests through [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) for caching, rate limiting, analytics, and cost tracking. AI Gateway supports multiple providers — configure your preferred provider in the gateway and use these env vars: +You can route API requests through [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) for caching, rate limiting, analytics, and cost tracking. OpenClaw has native support for Cloudflare AI Gateway as a first-class provider. + +AI Gateway acts as a proxy between OpenClaw and your AI provider (e.g., Anthropic). Requests are sent to `https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic` instead of directly to `api.anthropic.com`, giving you Cloudflare's analytics, caching, and rate limiting. You still need a provider API key (e.g., your Anthropic API key) — the gateway forwards it to the upstream provider. ### Setup 1. Create an AI Gateway in the [AI Gateway section](https://dash.cloudflare.com/?to=/:account/ai/ai-gateway/create-gateway) of the Cloudflare Dashboard. -2. Add a provider (e.g., Anthropic) to your gateway -3. Set the gateway secrets: - -You'll find the base URL on the Overview tab of your newly created gateway. At the bottom of the page, expand the **Native API/SDK Examples** section and select "Anthropic". +2. Set the three required secrets: ```bash -# Your provider's API key (e.g., Anthropic API key) -npx wrangler secret put AI_GATEWAY_API_KEY +# Your AI provider's API key (e.g., your Anthropic API key). +# This is passed through the gateway to the upstream provider. +npx wrangler secret put CLOUDFLARE_AI_GATEWAY_API_KEY -# Your AI Gateway endpoint URL -npx wrangler secret put AI_GATEWAY_BASE_URL -# Enter: https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic +# Your Cloudflare account ID +npx wrangler secret put CF_AI_GATEWAY_ACCOUNT_ID + +# Your AI Gateway ID (from the gateway overview page) +npx wrangler secret put CF_AI_GATEWAY_GATEWAY_ID ``` -4. Redeploy: +All three are required. OpenClaw constructs the gateway URL from the account ID and gateway ID, and passes the API key to the upstream provider through the gateway. + +3. Redeploy: ```bash npm run deploy ``` -The `AI_GATEWAY_*` variables take precedence over `ANTHROPIC_*` if both are set. +When Cloudflare AI Gateway is configured, it takes precedence over direct `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`. + +### Legacy AI Gateway Configuration + +The previous `AI_GATEWAY_API_KEY` + `AI_GATEWAY_BASE_URL` approach is still supported for backward compatibility but is deprecated in favor of the native configuration above. ## All Secrets Reference | Secret | Required | Description | |--------|----------|-------------| -| `AI_GATEWAY_API_KEY` | Yes* | API key for your AI Gateway provider (requires `AI_GATEWAY_BASE_URL`) | -| `AI_GATEWAY_BASE_URL` | Yes* | AI Gateway endpoint URL (required when using `AI_GATEWAY_API_KEY`) | -| `ANTHROPIC_API_KEY` | Yes* | Direct Anthropic API key (fallback if AI Gateway not configured) | -| `ANTHROPIC_BASE_URL` | No | Direct Anthropic API base URL (fallback) | +| `CLOUDFLARE_AI_GATEWAY_API_KEY` | Yes* | Your AI provider's API key, passed through the gateway (e.g., your Anthropic API key). Requires `CF_AI_GATEWAY_ACCOUNT_ID` and `CF_AI_GATEWAY_GATEWAY_ID` | +| `CF_AI_GATEWAY_ACCOUNT_ID` | Yes* | Your Cloudflare account ID (used to construct the gateway URL) | +| `CF_AI_GATEWAY_GATEWAY_ID` | Yes* | Your AI Gateway ID (used to construct the gateway URL) | +| `ANTHROPIC_API_KEY` | Yes* | Direct Anthropic API key (alternative to AI Gateway) | +| `ANTHROPIC_BASE_URL` | No | Direct Anthropic API base URL | | `OPENAI_API_KEY` | No | OpenAI API key (alternative provider) | +| `AI_GATEWAY_API_KEY` | No | Legacy AI Gateway API key (deprecated, use `CLOUDFLARE_AI_GATEWAY_API_KEY` instead) | +| `AI_GATEWAY_BASE_URL` | No | Legacy AI Gateway endpoint URL (deprecated) | | `CF_ACCESS_TEAM_DOMAIN` | Yes* | Cloudflare Access team domain (required for admin UI) | | `CF_ACCESS_AUD` | Yes* | Cloudflare Access application audience (required for admin UI) | | `MOLTBOT_GATEWAY_TOKEN` | Yes | Gateway token for authentication (pass via `?token=` query param) | From 021a9ed3452c4736567338aa5396fa194401da4a Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 16:36:47 +0100 Subject: [PATCH 2/8] feat: support arbitrary AI Gateway models via CF_AI_GATEWAY_MODEL Add CF_AI_GATEWAY_MODEL env var (format: provider/model-id) that lets users choose any model through Cloudflare AI Gateway. The startup script parses the provider slug, constructs the correct gateway URL, picks the API type (anthropic-messages or openai-completions), and patches the OpenClaw config. Examples: workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast openai/gpt-4o anthropic/claude-sonnet-4-5 groq/llama-3.3-70b For Workers AI without an AI Gateway, falls back to the direct Cloudflare API using CF_ACCOUNT_ID. Changes: - types.ts: add CF_AI_GATEWAY_MODEL to MoltbotEnv - env.ts: pass CF_AI_GATEWAY_MODEL and CF_ACCOUNT_ID to container - env.test.ts: tests for new passthrough - start-openclaw.sh: config patch for provider + default model - README.md: document model selection, provider table, secrets ref - .dev.vars.example: add CF_AI_GATEWAY_MODEL --- .dev.vars.example | 3 ++- README.md | 25 +++++++++++++++++++++++ src/gateway/env.test.ts | 15 ++++++++++++++ src/gateway/env.ts | 2 ++ src/types.ts | 1 + start-openclaw.sh | 45 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 1 deletion(-) diff --git a/.dev.vars.example b/.dev.vars.example index ad4a151a..5fc7dca6 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -6,9 +6,10 @@ ANTHROPIC_API_KEY=sk-ant-... # OPENAI_API_KEY=sk-... # Cloudflare AI Gateway (alternative to direct provider keys) -# CLOUDFLARE_AI_GATEWAY_API_KEY=your-gateway-api-key +# CLOUDFLARE_AI_GATEWAY_API_KEY=your-provider-api-key # CF_AI_GATEWAY_ACCOUNT_ID=your-account-id # CF_AI_GATEWAY_GATEWAY_ID=your-gateway-id +# CF_AI_GATEWAY_MODEL=workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast # Legacy AI Gateway (still supported) # AI_GATEWAY_API_KEY=your-key diff --git a/README.md b/README.md index 73c9389f..ea82f03a 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,30 @@ npm run deploy When Cloudflare AI Gateway is configured, it takes precedence over direct `ANTHROPIC_API_KEY` or `OPENAI_API_KEY`. +### Choosing a Model + +By default, AI Gateway uses Anthropic's Claude Sonnet 4.5. To use a different model or provider, set `CF_AI_GATEWAY_MODEL` with the format `provider/model-id`: + +```bash +npx wrangler secret put CF_AI_GATEWAY_MODEL +# Enter: workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast +``` + +This works with any [AI Gateway provider](https://developers.cloudflare.com/ai-gateway/usage/providers/): + +| Provider | Example `CF_AI_GATEWAY_MODEL` value | API key is... | +|----------|-------------------------------------|---------------| +| Workers AI | `workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast` | Cloudflare API token | +| OpenAI | `openai/gpt-4o` | OpenAI API key | +| Anthropic | `anthropic/claude-sonnet-4-5` | Anthropic API key | +| Groq | `groq/llama-3.3-70b` | Groq API key | + +**Note:** `CLOUDFLARE_AI_GATEWAY_API_KEY` must match the provider you're using — it's your provider's API key, forwarded through the gateway. You can only use one provider at a time through the gateway. For multiple providers, use direct keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`) alongside the gateway config. + +#### Workers AI with Unified Billing + +With [Unified Billing](https://developers.cloudflare.com/ai-gateway/features/unified-billing/), you can use Workers AI models without a separate provider API key — Cloudflare bills you directly. Set `CLOUDFLARE_AI_GATEWAY_API_KEY` to your [AI Gateway authentication token](https://developers.cloudflare.com/ai-gateway/configuration/authentication/) (the `cf-aig-authorization` token). + ### Legacy AI Gateway Configuration The previous `AI_GATEWAY_API_KEY` + `AI_GATEWAY_BASE_URL` approach is still supported for backward compatibility but is deprecated in favor of the native configuration above. @@ -391,6 +415,7 @@ The previous `AI_GATEWAY_API_KEY` + `AI_GATEWAY_BASE_URL` approach is still supp | `CLOUDFLARE_AI_GATEWAY_API_KEY` | Yes* | Your AI provider's API key, passed through the gateway (e.g., your Anthropic API key). Requires `CF_AI_GATEWAY_ACCOUNT_ID` and `CF_AI_GATEWAY_GATEWAY_ID` | | `CF_AI_GATEWAY_ACCOUNT_ID` | Yes* | Your Cloudflare account ID (used to construct the gateway URL) | | `CF_AI_GATEWAY_GATEWAY_ID` | Yes* | Your AI Gateway ID (used to construct the gateway URL) | +| `CF_AI_GATEWAY_MODEL` | No | Override default model: `provider/model-id` (e.g. `workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast`). See [Choosing a Model](#choosing-a-model) | | `ANTHROPIC_API_KEY` | Yes* | Direct Anthropic API key (alternative to AI Gateway) | | `ANTHROPIC_BASE_URL` | No | Direct Anthropic API base URL | | `OPENAI_API_KEY` | No | OpenAI API key (alternative provider) | diff --git a/src/gateway/env.test.ts b/src/gateway/env.test.ts index d0afd817..89af2efb 100644 --- a/src/gateway/env.test.ts +++ b/src/gateway/env.test.ts @@ -129,6 +129,21 @@ describe('buildEnvVars', () => { expect(result.OPENCLAW_DEV_MODE).toBe('true'); }); + // AI Gateway model override + it('passes CF_AI_GATEWAY_MODEL to container', () => { + const env = createMockEnv({ + CF_AI_GATEWAY_MODEL: 'workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast', + }); + const result = buildEnvVars(env); + expect(result.CF_AI_GATEWAY_MODEL).toBe('workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast'); + }); + + it('passes CF_ACCOUNT_ID to container', () => { + const env = createMockEnv({ CF_ACCOUNT_ID: 'acct-123' }); + const result = buildEnvVars(env); + expect(result.CF_ACCOUNT_ID).toBe('acct-123'); + }); + it('combines all env vars correctly', () => { const env = createMockEnv({ ANTHROPIC_API_KEY: 'sk-key', diff --git a/src/gateway/env.ts b/src/gateway/env.ts index 30dafeb8..23dea539 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -45,6 +45,8 @@ export function buildEnvVars(env: MoltbotEnv): Record { if (env.DISCORD_DM_POLICY) envVars.DISCORD_DM_POLICY = env.DISCORD_DM_POLICY; if (env.SLACK_BOT_TOKEN) envVars.SLACK_BOT_TOKEN = env.SLACK_BOT_TOKEN; if (env.SLACK_APP_TOKEN) envVars.SLACK_APP_TOKEN = env.SLACK_APP_TOKEN; + if (env.CF_AI_GATEWAY_MODEL) envVars.CF_AI_GATEWAY_MODEL = env.CF_AI_GATEWAY_MODEL; + if (env.CF_ACCOUNT_ID) envVars.CF_ACCOUNT_ID = env.CF_ACCOUNT_ID; if (env.CDP_SECRET) envVars.CDP_SECRET = env.CDP_SECRET; if (env.WORKER_URL) envVars.WORKER_URL = env.WORKER_URL; diff --git a/src/types.ts b/src/types.ts index c700d895..a85d32da 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,6 +11,7 @@ export interface MoltbotEnv { CF_AI_GATEWAY_ACCOUNT_ID?: string; // Cloudflare account ID for AI Gateway CF_AI_GATEWAY_GATEWAY_ID?: string; // AI Gateway ID CLOUDFLARE_AI_GATEWAY_API_KEY?: string; // API key for requests through the gateway + CF_AI_GATEWAY_MODEL?: string; // Override model: "provider/model-id" e.g. "workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast" // Legacy AI Gateway configuration (still supported for backward compat) AI_GATEWAY_API_KEY?: string; // API key for the provider configured in AI Gateway AI_GATEWAY_BASE_URL?: string; // AI Gateway URL (e.g., https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}/anthropic) diff --git a/start-openclaw.sh b/start-openclaw.sh index 6be5a351..09a131c6 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -201,6 +201,51 @@ if (process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_API_KEY) { console.log('Patched Anthropic provider with base URL:', baseUrl); } +// AI Gateway model override (CF_AI_GATEWAY_MODEL=provider/model-id) +// Adds a provider entry for any AI Gateway provider and sets it as default model. +// Examples: +// workers-ai/@cf/meta/llama-3.3-70b-instruct-fp8-fast +// openai/gpt-4o +// anthropic/claude-sonnet-4-5 +if (process.env.CF_AI_GATEWAY_MODEL) { + const raw = process.env.CF_AI_GATEWAY_MODEL; + const slashIdx = raw.indexOf('/'); + const gwProvider = raw.substring(0, slashIdx); + const modelId = raw.substring(slashIdx + 1); + + const accountId = process.env.CF_AI_GATEWAY_ACCOUNT_ID; + const gatewayId = process.env.CF_AI_GATEWAY_GATEWAY_ID; + const apiKey = process.env.CLOUDFLARE_AI_GATEWAY_API_KEY; + + let baseUrl; + if (accountId && gatewayId) { + baseUrl = 'https://gateway.ai.cloudflare.com/v1/' + accountId + '/' + gatewayId + '/' + gwProvider; + if (gwProvider === 'workers-ai') baseUrl += '/v1'; + } else if (gwProvider === 'workers-ai' && process.env.CF_ACCOUNT_ID) { + baseUrl = 'https://api.cloudflare.com/client/v4/accounts/' + process.env.CF_ACCOUNT_ID + '/ai/v1'; + } + + if (baseUrl && apiKey) { + const api = gwProvider === 'anthropic' ? 'anthropic-messages' : 'openai-completions'; + const providerName = 'cf-ai-gw-' + gwProvider; + + config.models = config.models || {}; + config.models.providers = config.models.providers || {}; + config.models.providers[providerName] = { + baseUrl: baseUrl, + apiKey: apiKey, + api: api, + models: [{ id: modelId, name: modelId, contextWindow: 131072, maxTokens: 8192 }], + }; + config.agents = config.agents || {}; + config.agents.defaults = config.agents.defaults || {}; + config.agents.defaults.model = { primary: providerName + '/' + modelId }; + console.log('AI Gateway model override: provider=' + providerName + ' model=' + modelId + ' via ' + baseUrl); + } else { + console.warn('CF_AI_GATEWAY_MODEL set but missing required config (account ID, gateway ID, or API key)'); + } +} + // Telegram configuration // Overwrite entire channel object to drop stale keys from old R2 backups // that would fail OpenClaw's strict config validation (see #47) From f7cd9603e87fde3b9e383ad4299602f4228dec43 Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 17:02:33 +0100 Subject: [PATCH 3/8] ci: update AI Gateway secrets and add workers-ai matrix config - Add new secrets: CLOUDFLARE_AI_GATEWAY_API_KEY, CF_AI_GATEWAY_ACCOUNT_ID, CF_AI_GATEWAY_GATEWAY_ID (keep legacy AI_GATEWAY_* for compat) - Pass CF_AI_GATEWAY_MODEL from matrix to env - Add 'workers-ai' matrix entry with CF_AI_GATEWAY_MODEL=workers-ai/@cf/openai/gpt-oss-120b --- .github/workflows/test.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a93f58ae..69a814a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,6 +56,9 @@ jobs: env: DISCORD_BOT_TOKEN: "fake-discord-bot-token-for-e2e" DISCORD_DM_POLICY: "pairing" + - name: workers-ai + env: + CF_AI_GATEWAY_MODEL: "workers-ai/@cf/openai/gpt-oss-120b" name: e2e (${{ matrix.config.name }}) @@ -98,7 +101,11 @@ jobs: CF_ACCESS_TEAM_DOMAIN: ${{ secrets.E2E_CF_ACCESS_TEAM_DOMAIN }} R2_ACCESS_KEY_ID: ${{ secrets.E2E_R2_ACCESS_KEY_ID }} R2_SECRET_ACCESS_KEY: ${{ secrets.E2E_R2_SECRET_ACCESS_KEY }} - # AI provider (optional, for chat tests) + # AI provider — Cloudflare AI Gateway (preferred) + CLOUDFLARE_AI_GATEWAY_API_KEY: ${{ secrets.CLOUDFLARE_AI_GATEWAY_API_KEY }} + CF_AI_GATEWAY_ACCOUNT_ID: ${{ secrets.CF_AI_GATEWAY_ACCOUNT_ID }} + CF_AI_GATEWAY_GATEWAY_ID: ${{ secrets.CF_AI_GATEWAY_GATEWAY_ID }} + # AI provider — legacy (still supported) AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }} AI_GATEWAY_BASE_URL: ${{ secrets.AI_GATEWAY_BASE_URL }} # Unique test run ID for parallel isolation @@ -108,6 +115,7 @@ jobs: TELEGRAM_DM_POLICY: ${{ matrix.config.env.TELEGRAM_DM_POLICY }} DISCORD_BOT_TOKEN: ${{ matrix.config.env.DISCORD_BOT_TOKEN }} DISCORD_DM_POLICY: ${{ matrix.config.env.DISCORD_DM_POLICY }} + CF_AI_GATEWAY_MODEL: ${{ matrix.config.env.CF_AI_GATEWAY_MODEL }} run: cctr -vv test/e2e - name: Convert video and generate thumbnail From 1a3c1189ab2c599b7f8820bb2f7a87f2c8f866e8 Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 17:58:12 +0100 Subject: [PATCH 4/8] fix: remove broken legacy anthropic provider patching and config leak - Remove legacy ANTHROPIC_BASE_URL provider patching that creates a provider entry without a models array, breaking OpenClaw v2026.2.3 config validation. The Anthropic SDK picks up ANTHROPIC_BASE_URL from the environment natively. - Remove console.log that dumps the full config (including API keys and gateway tokens) to stdout. --- start-openclaw.sh | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/start-openclaw.sh b/start-openclaw.sh index 09a131c6..dd9381d1 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -189,17 +189,10 @@ if (process.env.OPENCLAW_DEV_MODE === 'true') { config.gateway.controlUi.allowInsecureAuth = true; } -// Legacy AI Gateway base URL override — patch into provider config -// (only needed when using AI_GATEWAY_BASE_URL, not native cloudflare-ai-gateway) -if (process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_API_KEY) { - const baseUrl = process.env.ANTHROPIC_BASE_URL.replace(/\/+$/, ''); - config.models = config.models || {}; - config.models.providers = config.models.providers || {}; - config.models.providers.anthropic = config.models.providers.anthropic || {}; - config.models.providers.anthropic.baseUrl = baseUrl; - config.models.providers.anthropic.apiKey = process.env.ANTHROPIC_API_KEY; - console.log('Patched Anthropic provider with base URL:', baseUrl); -} +// Legacy AI Gateway base URL override: +// ANTHROPIC_BASE_URL is picked up natively by the Anthropic SDK, +// so we don't need to patch the provider config. Writing a provider +// entry without a models array breaks OpenClaw's config validation. // AI Gateway model override (CF_AI_GATEWAY_MODEL=provider/model-id) // Adds a provider entry for any AI Gateway provider and sets it as default model. @@ -289,7 +282,6 @@ if (process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) { fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); console.log('Configuration patched successfully'); -console.log('Config:', JSON.stringify(config, null, 2)); EOFPATCH # ============================================================ From d3417d05f2e217b04502e3f59f3b8062bbbd7ecc Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 19:23:47 +0100 Subject: [PATCH 5/8] fix(e2e): push new AI Gateway secrets to deployed worker The deploy script was only pushing legacy AI_GATEWAY_* secrets. Add CLOUDFLARE_AI_GATEWAY_API_KEY, CF_AI_GATEWAY_ACCOUNT_ID, CF_AI_GATEWAY_GATEWAY_ID, and CF_AI_GATEWAY_MODEL. --- test/e2e/fixture/server/deploy | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/e2e/fixture/server/deploy b/test/e2e/fixture/server/deploy index f139dddd..8c18666f 100755 --- a/test/e2e/fixture/server/deploy +++ b/test/e2e/fixture/server/deploy @@ -70,12 +70,27 @@ echo "$R2_BUCKET" | npx wrangler secret put R2_BUCKET_NAME --name "$WORKER_NAME" echo "$CLOUDFLARE_ACCOUNT_ID" | npx wrangler secret put CF_ACCOUNT_ID --name "$WORKER_NAME" >&2 # Set AI provider keys if available +# Cloudflare AI Gateway (preferred) +if [ -n "$CLOUDFLARE_AI_GATEWAY_API_KEY" ]; then + echo "$CLOUDFLARE_AI_GATEWAY_API_KEY" | npx wrangler secret put CLOUDFLARE_AI_GATEWAY_API_KEY --name "$WORKER_NAME" >&2 +fi +if [ -n "$CF_AI_GATEWAY_ACCOUNT_ID" ]; then + echo "$CF_AI_GATEWAY_ACCOUNT_ID" | npx wrangler secret put CF_AI_GATEWAY_ACCOUNT_ID --name "$WORKER_NAME" >&2 +fi +if [ -n "$CF_AI_GATEWAY_GATEWAY_ID" ]; then + echo "$CF_AI_GATEWAY_GATEWAY_ID" | npx wrangler secret put CF_AI_GATEWAY_GATEWAY_ID --name "$WORKER_NAME" >&2 +fi +if [ -n "$CF_AI_GATEWAY_MODEL" ]; then + echo "$CF_AI_GATEWAY_MODEL" | npx wrangler secret put CF_AI_GATEWAY_MODEL --name "$WORKER_NAME" >&2 +fi +# Legacy AI Gateway (still supported) if [ -n "$AI_GATEWAY_API_KEY" ]; then echo "$AI_GATEWAY_API_KEY" | npx wrangler secret put AI_GATEWAY_API_KEY --name "$WORKER_NAME" >&2 fi if [ -n "$AI_GATEWAY_BASE_URL" ]; then echo "$AI_GATEWAY_BASE_URL" | npx wrangler secret put AI_GATEWAY_BASE_URL --name "$WORKER_NAME" >&2 fi +# Direct provider keys if [ -n "$ANTHROPIC_API_KEY" ]; then echo "$ANTHROPIC_API_KEY" | npx wrangler secret put ANTHROPIC_API_KEY --name "$WORKER_NAME" >&2 fi From 407acc2d148b64747b44c334012ee6eba1223016 Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 19:33:21 +0100 Subject: [PATCH 6/8] fix: pass --token to openclaw CLI commands for device management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenClaw v2026.2.3 requires explicit credentials when using --url override. All openclaw devices CLI invocations (list, approve, approve-all) now pass --token from MOLTBOT_GATEWAY_TOKEN. This was the root cause of 'Container service disconnected' errors and missing devices in the admin UI — the devices list CLI command was failing with 'gateway url override requires explicit credentials'. --- src/routes/api.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/routes/api.ts b/src/routes/api.ts index c002890b..bbc04feb 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -38,9 +38,11 @@ adminApi.get('/devices', async (c) => { await ensureMoltbotGateway(sandbox, c.env); // Run OpenClaw CLI to list devices - // Must specify --url to connect to the gateway running in the same container + // Must specify --url and --token (OpenClaw v2026.2.3 requires explicit credentials with --url) + const token = c.env.MOLTBOT_GATEWAY_TOKEN; + const tokenArg = token ? ` --token ${token}` : ''; const proc = await sandbox.startProcess( - 'openclaw devices list --json --url ws://localhost:18789', + `openclaw devices list --json --url ws://localhost:18789${tokenArg}`, ); await waitForProcess(proc, CLI_TIMEOUT_MS); @@ -93,8 +95,10 @@ adminApi.post('/devices/:requestId/approve', async (c) => { await ensureMoltbotGateway(sandbox, c.env); // Run OpenClaw CLI to approve the device + const token = c.env.MOLTBOT_GATEWAY_TOKEN; + const tokenArg = token ? ` --token ${token}` : ''; const proc = await sandbox.startProcess( - `openclaw devices approve ${requestId} --url ws://localhost:18789`, + `openclaw devices approve ${requestId} --url ws://localhost:18789${tokenArg}`, ); await waitForProcess(proc, CLI_TIMEOUT_MS); @@ -127,8 +131,10 @@ adminApi.post('/devices/approve-all', async (c) => { await ensureMoltbotGateway(sandbox, c.env); // First, get the list of pending devices + const token = c.env.MOLTBOT_GATEWAY_TOKEN; + const tokenArg = token ? ` --token ${token}` : ''; const listProc = await sandbox.startProcess( - 'openclaw devices list --json --url ws://localhost:18789', + `openclaw devices list --json --url ws://localhost:18789${tokenArg}`, ); await waitForProcess(listProc, CLI_TIMEOUT_MS); @@ -158,7 +164,7 @@ adminApi.post('/devices/approve-all', async (c) => { try { // eslint-disable-next-line no-await-in-loop -- sequential device approval required const approveProc = await sandbox.startProcess( - `openclaw devices approve ${device.requestId} --url ws://localhost:18789`, + `openclaw devices approve ${device.requestId} --url ws://localhost:18789${tokenArg}`, ); // eslint-disable-next-line no-await-in-loop await waitForProcess(approveProc, CLI_TIMEOUT_MS); From 0ec3555879640d1bc8177b84bc2e8ce63465374e Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 19:37:01 +0100 Subject: [PATCH 7/8] fix(e2e): increase worker ready timeout from 5 to 8 minutes --- test/e2e/_setup.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/_setup.txt b/test/e2e/_setup.txt index 63d907aa..4438a801 100644 --- a/test/e2e/_setup.txt +++ b/test/e2e/_setup.txt @@ -34,7 +34,7 @@ WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt") ./pw --session=moltworker-e2e open "$WORKER_URL/?token=$TOKEN" # Wait for pairing required message (worker shows loading screen first, then UI loads) ./pw --session=moltworker-e2e run-code "async page => { - await page.waitForSelector('text=Pairing required', { timeout: 300000 }); + await page.waitForSelector('text=Pairing required', { timeout: 480000 }); }" echo "Worker is ready" --- From 1bd08a497e27b5a865455a34391ff43f9edc05be Mon Sep 17 00:00:00 2001 From: Andreas Jansson Date: Fri, 6 Feb 2026 20:16:50 +0100 Subject: [PATCH 8/8] fix(e2e): dump gateway process logs on teardown for debugging --- test/e2e/_teardown.txt | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/e2e/_teardown.txt b/test/e2e/_teardown.txt index 1b7888e6..6e914b49 100644 --- a/test/e2e/_teardown.txt +++ b/test/e2e/_teardown.txt @@ -1,3 +1,34 @@ +=== +dump gateway logs for debugging +=== +WORKER_URL=$(cat "$CCTR_FIXTURE_DIR/worker-url.txt" 2>/dev/null || echo "") +if [ -n "$WORKER_URL" ]; then + PROCS=$(./curl-auth -s "$WORKER_URL/debug/processes" 2>/dev/null || echo "") + PROC_ID=$(echo "$PROCS" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) + if [ -n "$PROC_ID" ]; then + echo "=== Gateway process logs ($PROC_ID) ===" + ./curl-auth -s "$WORKER_URL/debug/logs?id=$PROC_ID" 2>/dev/null | python3 -c " +import sys, json +try: + d = json.load(sys.stdin) + if d.get('stdout'): print('STDOUT:', d['stdout'][-3000:]) + if d.get('stderr'): print('STDERR:', d['stderr'][-3000:]) +except: print('Failed to parse logs') +" || echo "Failed to fetch logs" + else + echo "No gateway process found" + echo "Processes: $PROCS" + fi +else + echo "No worker URL found" +fi +echo "dump complete" +--- +{{ output }} +--- +where +* output contains "dump complete" + === stop video recording ===