From 991f1321950cd4bacaae3a10ea26d2cc79754c4a Mon Sep 17 00:00:00 2001 From: Jeff Boles <1313120+jbbjbb@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:12:08 -0700 Subject: [PATCH] feat: add CF_AIG_TOKEN support for AI Gateway Authenticated Gateway Pass CF_AIG_TOKEN from Worker environment to container, enabling AI Gateway Authenticated Gateway and BYOK (Bring Your Own Key). OpenClaw's config schema does not support custom headers on provider configs, so a Node.js --require hook patches globalThis.fetch to inject the cf-aig-authorization header for gateway.ai.cloudflare.com requests. This was discovered during live testing on a deployed Cloudflare Sandbox instance. Security hardening (scoped to touched files): - chmod 600 on rclone.conf (shell script + Worker-side r2.ts) - chmod 600 on openclaw.json after config patch - Remove redundant gateway token from config file (already passed via --token CLI flag) - Input validation on CF_AIG_TOKEN for control characters Fixes #74 Duplicates: #119 Related: #192 Co-Authored-By: Claude Opus 4.6 --- .dev.vars.example | 1 + AGENTS.md | 1 + README.md | 1 + src/gateway/env.test.ts | 17 +++++++++++++++++ src/gateway/env.ts | 14 ++++++++++++++ src/gateway/r2.ts | 1 + src/types.ts | 1 + start-openclaw.sh | 32 ++++++++++++++++++++++++++++---- wrangler.jsonc | 1 + 9 files changed, 65 insertions(+), 4 deletions(-) diff --git a/.dev.vars.example b/.dev.vars.example index 5fc7dca6f..e4eecb6ea 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -10,6 +10,7 @@ ANTHROPIC_API_KEY=sk-ant-... # 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 +# CF_AIG_TOKEN=your-ai-gateway-auth-token # Legacy AI Gateway (still supported) # AI_GATEWAY_API_KEY=your-key diff --git a/AGENTS.md b/AGENTS.md index ec30d2c18..ed1ad70f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,6 +201,7 @@ These are the env vars passed TO the container (internal names): | `CF_AI_GATEWAY_ACCOUNT_ID` | (env var) | Account ID for AI Gateway | | `CF_AI_GATEWAY_GATEWAY_ID` | (env var) | Gateway ID for AI Gateway | | `OPENCLAW_GATEWAY_TOKEN` | `--token` flag | Mapped from `MOLTBOT_GATEWAY_TOKEN` | +| `CF_AIG_TOKEN` | `cf-aig-authorization` header via fetch hook | AI Gateway auth token, injected for `gateway.ai.cloudflare.com` requests | | `OPENCLAW_DEV_MODE` | `controlUi.allowInsecureAuth` | Mapped from `DEV_MODE` | | `TELEGRAM_BOT_TOKEN` | `channels.telegram.botToken` | | | `DISCORD_BOT_TOKEN` | `channels.discord.token` | | diff --git a/README.md b/README.md index ea82f03af..ee179ec28 100644 --- a/README.md +++ b/README.md @@ -416,6 +416,7 @@ The previous `AI_GATEWAY_API_KEY` + `AI_GATEWAY_BASE_URL` approach is still supp | `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) | +| `CF_AIG_TOKEN` | No | AI Gateway authentication token. Enables [Authenticated Gateway](https://developers.cloudflare.com/ai-gateway/configuration/authentication/) and BYOK. Create in CF Dashboard > AI > AI Gateway > Settings > Create token | | `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 89af2efb8..e820a3c0d 100644 --- a/src/gateway/env.test.ts +++ b/src/gateway/env.test.ts @@ -144,6 +144,23 @@ describe('buildEnvVars', () => { expect(result.CF_ACCOUNT_ID).toBe('acct-123'); }); + it('passes CF_AIG_TOKEN to container', () => { + const env = createMockEnv({ CF_AIG_TOKEN: 'aig-token-abc123' }); + const result = buildEnvVars(env); + expect(result.CF_AIG_TOKEN).toBe('aig-token-abc123'); + }); + + it('does not include CF_AIG_TOKEN when not set', () => { + const env = createMockEnv(); + const result = buildEnvVars(env); + expect(result.CF_AIG_TOKEN).toBeUndefined(); + }); + + it('rejects CF_AIG_TOKEN with control characters', () => { + const env = createMockEnv({ CF_AIG_TOKEN: 'token\x00injected' }); + expect(() => buildEnvVars(env)).toThrow('invalid control characters'); + }); + 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 d9e01171b..576206897 100644 --- a/src/gateway/env.ts +++ b/src/gateway/env.ts @@ -1,5 +1,18 @@ import type { MoltbotEnv } from '../types'; +/** + * Validation: Environment variables passed to the container should be + * validated for control characters that could cause injection or parsing + * issues. Use validateEnvValue() for any new sensitive token additions. + */ +function validateEnvValue(value: string): string { + // eslint-disable-next-line no-control-regex -- intentionally matching control characters + if (/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)) { + throw new Error('Environment variable contains invalid control characters'); + } + return value; +} + /** * Build environment variables to pass to the OpenClaw container process * @@ -46,6 +59,7 @@ export function buildEnvVars(env: MoltbotEnv): Record { 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_AIG_TOKEN) envVars.CF_AIG_TOKEN = validateEnvValue(env.CF_AIG_TOKEN); 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/gateway/r2.ts b/src/gateway/r2.ts index a506654e3..46b314bf9 100644 --- a/src/gateway/r2.ts +++ b/src/gateway/r2.ts @@ -37,6 +37,7 @@ export async function ensureRcloneConfig(sandbox: Sandbox, env: MoltbotEnv): Pro await sandbox.exec(`mkdir -p $(dirname ${RCLONE_CONF_PATH})`); await sandbox.writeFile(RCLONE_CONF_PATH, rcloneConfig); + await sandbox.exec(`chmod 600 ${RCLONE_CONF_PATH}`); await sandbox.exec(`touch ${CONFIGURED_FLAG}`); console.log('Rclone configured for R2 bucket:', getR2BucketName(env)); diff --git a/src/types.ts b/src/types.ts index a85d32da3..3c01b762b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -12,6 +12,7 @@ export interface MoltbotEnv { 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" + CF_AIG_TOKEN?: string; // AI Gateway authentication token (cf-aig-authorization header) // 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 c862a80ce..0ac578930 100644 --- a/start-openclaw.sh +++ b/start-openclaw.sh @@ -47,6 +47,7 @@ endpoint = https://${CF_ACCOUNT_ID}.r2.cloudflarestorage.com acl = private no_check_bucket = true EOF + chmod 600 "$RCLONE_CONF" touch /tmp/.rclone-configured echo "Rclone configured for bucket: $R2_BUCKET" } @@ -159,10 +160,7 @@ config.gateway.port = 18789; config.gateway.mode = 'local'; config.gateway.trustedProxies = ['10.1.0.0']; -if (process.env.OPENCLAW_GATEWAY_TOKEN) { - config.gateway.auth = config.gateway.auth || {}; - config.gateway.auth.token = process.env.OPENCLAW_GATEWAY_TOKEN; -} +// Gateway token is passed via --token CLI flag (line 325), not written to config file. if (process.env.OPENCLAW_DEV_MODE === 'true') { config.gateway.controlUi = config.gateway.controlUi || {}; @@ -261,6 +259,7 @@ if (process.env.SLACK_BOT_TOKEN && process.env.SLACK_APP_TOKEN) { } fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); +fs.chmodSync(configPath, 0o600); console.log('Configuration patched successfully'); EOFPATCH @@ -309,6 +308,31 @@ if r2_configured; then echo "Background sync loop started (PID: $!)" fi +# ============================================================ +# AI GATEWAY AUTHENTICATION HOOK +# ============================================================ +# OpenClaw's config schema does not support custom headers on providers. +# To send the cf-aig-authorization header required by AI Gateway +# Authenticated Gateway / BYOK, we install a Node.js --require hook +# that patches globalThis.fetch for requests to gateway.ai.cloudflare.com. +if [ -n "$CF_AIG_TOKEN" ]; then + cat > /tmp/aig-auth-hook.cjs << 'EOFHOOK' +const _fetch = globalThis.fetch; +globalThis.fetch = function (input, init) { + const url = typeof input === 'string' ? input : (input && input.url) || ''; + if (process.env.CF_AIG_TOKEN && url.includes('gateway.ai.cloudflare.com')) { + init = Object.assign({}, init); + const h = new Headers(init.headers); + h.set('cf-aig-authorization', 'Bearer ' + process.env.CF_AIG_TOKEN); + init.headers = h; + } + return _fetch.call(this, input, init); +}; +EOFHOOK + export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /tmp/aig-auth-hook.cjs" + echo "AI Gateway authentication hook installed" +fi + # ============================================================ # START GATEWAY # ============================================================ diff --git a/wrangler.jsonc b/wrangler.jsonc index 7b2ce8d0b..6630b0ba3 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -80,6 +80,7 @@ // - Legacy AI Gateway (still supported): // - AI_GATEWAY_API_KEY: API key // - AI_GATEWAY_BASE_URL: Gateway endpoint URL + // - CF_AIG_TOKEN: AI Gateway authentication token (enables Authenticated Gateway + BYOK) // // Authentication: // - MOLTBOT_GATEWAY_TOKEN: Token to protect gateway access