Support Nginx-authenticated OpenClaw and fix workflow retry start context

This commit is contained in:
2026-02-27 11:42:50 -05:00
parent 7c3836068f
commit 73d9ccafd6
4 changed files with 114 additions and 12 deletions

View File

@@ -88,6 +88,12 @@ BETTER_AUTH_TRUSTED_ORIGINS=https://fiscal.b11studio.xyz
OPENCLAW_BASE_URL=http://localhost:4000 OPENCLAW_BASE_URL=http://localhost:4000
OPENCLAW_API_KEY=your_key OPENCLAW_API_KEY=your_key
OPENCLAW_MODEL=zeroclaw 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 <support@fiscal.local> SEC_USER_AGENT=Fiscal Clone <support@fiscal.local>
WORKFLOW_TARGET_WORLD=local WORKFLOW_TARGET_WORLD=local
@@ -95,7 +101,19 @@ WORKFLOW_LOCAL_DATA_DIR=.workflow-data
WORKFLOW_LOCAL_QUEUE_CONCURRENCY=100 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 ## API surface

View File

@@ -28,7 +28,7 @@ export async function runTaskWorkflow(taskId: string) {
if (nextState.shouldRetry) { if (nextState.shouldRetry) {
await sleep('1200ms'); 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'; 'use step';
return await markTaskFailure(taskId, reason); return await markTaskFailure(taskId, reason);
} }
async function restartTaskWorkflowStep(taskId: string) {
'use step';
await start(runTaskWorkflow, [taskId]);
}

View File

@@ -21,6 +21,10 @@ services:
OPENCLAW_BASE_URL: ${OPENCLAW_BASE_URL:-} OPENCLAW_BASE_URL: ${OPENCLAW_BASE_URL:-}
OPENCLAW_API_KEY: ${OPENCLAW_API_KEY:-} OPENCLAW_API_KEY: ${OPENCLAW_API_KEY:-}
OPENCLAW_MODEL: ${OPENCLAW_MODEL:-zeroclaw} 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 <support@fiscal.local>} SEC_USER_AGENT: ${SEC_USER_AGENT:-Fiscal Clone <support@fiscal.local>}
WORKFLOW_TARGET_WORLD: local WORKFLOW_TARGET_WORLD: local
WORKFLOW_LOCAL_DATA_DIR: ${WORKFLOW_LOCAL_DATA_DIR:-/app/.workflow-data} WORKFLOW_LOCAL_DATA_DIR: ${WORKFLOW_LOCAL_DATA_DIR:-/app/.workflow-data}

View File

@@ -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) { function envValue(name: string) {
const value = process.env[name]; const value = process.env[name];
if (!value) { if (!value) {
@@ -17,12 +29,73 @@ function envValue(name: string) {
} }
const DEFAULT_MODEL = 'zeroclaw'; 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<string, string> = {
'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) { function fallbackResponse(prompt: string) {
const clipped = prompt.split('\n').slice(0, 6).join(' ').slice(0, 260); const clipped = prompt.split('\n').slice(0, 6).join(' ').slice(0, 260);
return [ 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.', '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.', 'Risk scan: Concentration and filing sentiment should be monitored after each sync cycle.',
`Context digest: ${clipped}` `Context digest: ${clipped}`
@@ -33,19 +106,24 @@ export function getOpenClawConfig() {
return { return {
baseUrl: envValue('OPENCLAW_BASE_URL'), baseUrl: envValue('OPENCLAW_BASE_URL'),
apiKey: envValue('OPENCLAW_API_KEY'), 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() { export function isOpenClawConfigured() {
const config = getOpenClawConfig(); 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) { export async function runOpenClawAnalysis(prompt: string, systemPrompt?: string) {
const config = getOpenClawConfig(); const config = getOpenClawConfig();
const endpoint = config.baseUrl ? buildCompletionsUrl(config.baseUrl) : undefined;
if (!config.baseUrl || !config.apiKey) { if (!endpoint || !hasRequiredAuth(config)) {
return { return {
provider: 'local-fallback', provider: 'local-fallback',
model: config.model, 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', method: 'POST',
headers: { headers: buildAuthHeaders(config),
'Content-Type': 'application/json',
Authorization: `Bearer ${config.apiKey}`
},
body: JSON.stringify({ body: JSON.stringify({
model: config.model, model: config.model,
temperature: 0.2, temperature: 0.2,