From 73d9ccafd6ba64e0b2f978019249878fd1d0803d Mon Sep 17 00:00:00 2001 From: francy51 Date: Fri, 27 Feb 2026 11:42:50 -0500 Subject: [PATCH] Support Nginx-authenticated OpenClaw and fix workflow retry start context --- README.md | 20 +++++++- app/workflows/task-runner.ts | 7 ++- docker-compose.yml | 4 ++ lib/server/openclaw.ts | 95 ++++++++++++++++++++++++++++++++---- 4 files changed, 114 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 47c3820..0701c4a 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,12 @@ BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz OPENCLAW_BASE_URL=http://localhost:4000 OPENCLAW_API_KEY=your_key OPENCLAW_MODEL=zeroclaw +OPENCLAW_AUTH_MODE=bearer +# for OPENCLAW_AUTH_MODE=basic +# OPENCLAW_BASIC_AUTH_USERNAME=your_nginx_user +# OPENCLAW_BASIC_AUTH_PASSWORD=your_nginx_password +# optional: forward API key in a custom header when using basic/none auth +# OPENCLAW_API_KEY_HEADER=X-OpenClaw-API-Key SEC_USER_AGENT=Fiscal Clone WORKFLOW_TARGET_WORLD=local @@ -95,7 +101,19 @@ WORKFLOW_LOCAL_DATA_DIR=.workflow-data WORKFLOW_LOCAL_QUEUE_CONCURRENCY=100 ``` -If OpenClaw is unset, the app uses local fallback analysis so task workflows still run. +If OpenClaw is unset or invalidly configured, the app uses local fallback analysis so task workflows still run. + +For OpenClaw behind Nginx Basic Auth, use: + +```env +OPENCLAW_BASE_URL=https://your-nginx-host +OPENCLAW_AUTH_MODE=basic +OPENCLAW_BASIC_AUTH_USERNAME=your_nginx_user +OPENCLAW_BASIC_AUTH_PASSWORD=your_nginx_password +# optional if upstream still needs an API key in a non-Authorization header +OPENCLAW_API_KEY=your_key +OPENCLAW_API_KEY_HEADER=X-OpenClaw-API-Key +``` ## API surface diff --git a/app/workflows/task-runner.ts b/app/workflows/task-runner.ts index c1de043..2df7882 100644 --- a/app/workflows/task-runner.ts +++ b/app/workflows/task-runner.ts @@ -28,7 +28,7 @@ export async function runTaskWorkflow(taskId: string) { if (nextState.shouldRetry) { await sleep('1200ms'); - await start(runTaskWorkflow, [task.id]); + await restartTaskWorkflowStep(task.id); } } } @@ -52,3 +52,8 @@ async function markTaskFailureStep(taskId: string, reason: string) { 'use step'; return await markTaskFailure(taskId, reason); } + +async function restartTaskWorkflowStep(taskId: string) { + 'use step'; + await start(runTaskWorkflow, [taskId]); +} diff --git a/docker-compose.yml b/docker-compose.yml index 9f0fd4f..b839f32 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,6 +21,10 @@ services: OPENCLAW_BASE_URL: ${OPENCLAW_BASE_URL:-} OPENCLAW_API_KEY: ${OPENCLAW_API_KEY:-} OPENCLAW_MODEL: ${OPENCLAW_MODEL:-zeroclaw} + OPENCLAW_AUTH_MODE: ${OPENCLAW_AUTH_MODE:-bearer} + OPENCLAW_BASIC_AUTH_USERNAME: ${OPENCLAW_BASIC_AUTH_USERNAME:-} + OPENCLAW_BASIC_AUTH_PASSWORD: ${OPENCLAW_BASIC_AUTH_PASSWORD:-} + OPENCLAW_API_KEY_HEADER: ${OPENCLAW_API_KEY_HEADER:-} SEC_USER_AGENT: ${SEC_USER_AGENT:-Fiscal Clone } WORKFLOW_TARGET_WORLD: local WORKFLOW_LOCAL_DATA_DIR: ${WORKFLOW_LOCAL_DATA_DIR:-/app/.workflow-data} diff --git a/lib/server/openclaw.ts b/lib/server/openclaw.ts index 6f1ecef..2750330 100644 --- a/lib/server/openclaw.ts +++ b/lib/server/openclaw.ts @@ -6,6 +6,18 @@ type ChatCompletionResponse = { }>; }; +type OpenClawAuthMode = 'bearer' | 'basic' | 'none'; + +type OpenClawConfig = { + baseUrl?: string; + apiKey?: string; + model: string; + authMode: OpenClawAuthMode; + basicAuthUsername?: string; + basicAuthPassword?: string; + apiKeyHeader?: string; +}; + function envValue(name: string) { const value = process.env[name]; if (!value) { @@ -17,12 +29,73 @@ function envValue(name: string) { } const DEFAULT_MODEL = 'zeroclaw'; +const DEFAULT_AUTH_MODE: OpenClawAuthMode = 'bearer'; + +function parseAuthMode(value: string | undefined): OpenClawAuthMode { + const normalized = value?.trim().toLowerCase(); + if (normalized === 'basic' || normalized === 'none') { + return normalized; + } + + return DEFAULT_AUTH_MODE; +} + +function hasSupportedProtocol(url: string) { + try { + const parsed = new URL(url); + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + } catch { + return false; + } +} + +function buildCompletionsUrl(baseUrl: string) { + if (!hasSupportedProtocol(baseUrl)) { + return undefined; + } + + const withSlash = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`; + return new URL('v1/chat/completions', withSlash).toString(); +} + +function hasRequiredAuth(config: OpenClawConfig) { + if (config.authMode === 'none') { + return true; + } + + if (config.authMode === 'basic') { + return Boolean(config.basicAuthUsername && config.basicAuthPassword); + } + + return Boolean(config.apiKey); +} + +function buildAuthHeaders(config: OpenClawConfig) { + const headers: Record = { + 'Content-Type': 'application/json' + }; + + if (config.authMode === 'basic' && config.basicAuthUsername && config.basicAuthPassword) { + const credentials = Buffer + .from(`${config.basicAuthUsername}:${config.basicAuthPassword}`, 'utf8') + .toString('base64'); + headers.Authorization = `Basic ${credentials}`; + } else if (config.authMode === 'bearer' && config.apiKey) { + headers.Authorization = `Bearer ${config.apiKey}`; + } + + if (config.apiKey && config.apiKeyHeader) { + headers[config.apiKeyHeader] = config.apiKey; + } + + return headers; +} function fallbackResponse(prompt: string) { const clipped = prompt.split('\n').slice(0, 6).join(' ').slice(0, 260); return [ - 'OpenClaw fallback mode is active (missing OPENCLAW_BASE_URL or OPENCLAW_API_KEY).', + 'OpenClaw fallback mode is active (configuration is missing or invalid).', 'Thesis: Portfolio remains analyzable with local heuristics until live model access is configured.', 'Risk scan: Concentration and filing sentiment should be monitored after each sync cycle.', `Context digest: ${clipped}` @@ -33,19 +106,24 @@ export function getOpenClawConfig() { return { baseUrl: envValue('OPENCLAW_BASE_URL'), apiKey: envValue('OPENCLAW_API_KEY'), - model: envValue('OPENCLAW_MODEL') ?? DEFAULT_MODEL - }; + model: envValue('OPENCLAW_MODEL') ?? DEFAULT_MODEL, + authMode: parseAuthMode(envValue('OPENCLAW_AUTH_MODE')), + basicAuthUsername: envValue('OPENCLAW_BASIC_AUTH_USERNAME'), + basicAuthPassword: envValue('OPENCLAW_BASIC_AUTH_PASSWORD'), + apiKeyHeader: envValue('OPENCLAW_API_KEY_HEADER') + } satisfies OpenClawConfig; } export function isOpenClawConfigured() { const config = getOpenClawConfig(); - return Boolean(config.baseUrl && config.apiKey); + return Boolean(config.baseUrl && hasSupportedProtocol(config.baseUrl) && hasRequiredAuth(config)); } export async function runOpenClawAnalysis(prompt: string, systemPrompt?: string) { const config = getOpenClawConfig(); + const endpoint = config.baseUrl ? buildCompletionsUrl(config.baseUrl) : undefined; - if (!config.baseUrl || !config.apiKey) { + if (!endpoint || !hasRequiredAuth(config)) { return { provider: 'local-fallback', model: config.model, @@ -53,12 +131,9 @@ export async function runOpenClawAnalysis(prompt: string, systemPrompt?: string) }; } - const response = await fetch(`${config.baseUrl}/v1/chat/completions`, { + const response = await fetch(endpoint, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${config.apiKey}` - }, + headers: buildAuthHeaders(config), body: JSON.stringify({ model: config.model, temperature: 0.2,