From f0f22129be76901d4d78740b6bdebfa54710a412 Mon Sep 17 00:00:00 2001 From: nyk <93952610+0xNyk@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:19:34 +0700 Subject: [PATCH] fix: healthcheck auth, secure cookie auto-detect, model object crash (#84) Bug 1 (#78): Dockerfile HEALTHCHECK curled authenticated /api/status, always got 401 in production. Changed to /login which is public. Bug 2 (#78): Login hangs on HTTP deployments because secure=true cookie is silently rejected. Now auto-detects protocol from x-forwarded-proto header, only sets secure when request actually came over HTTPS. Bug 3 (#78): Agent model field from OpenClaw 2026.3.x is {primary: "name"} object instead of string, causing React error #31. Added normalizeModel() helper and applied it in all WebSocket/session mapping code paths. --- Dockerfile | 2 +- src/app/api/auth/google/route.ts | 5 ++++- src/app/api/auth/login/route.ts | 5 ++++- src/components/dashboard/agent-network.tsx | 2 +- src/lib/session-cookie.ts | 6 ++++-- src/lib/sessions.ts | 2 +- src/lib/utils.ts | 7 +++++++ src/lib/websocket.ts | 7 ++++--- 8 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index a9a85a3..b5e7c55 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,5 +29,5 @@ EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME=0.0.0.0 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:3000/api/status || exit 1 + CMD curl -f http://localhost:3000/login || exit 1 CMD ["node", "server.js"] diff --git a/src/app/api/auth/google/route.ts b/src/app/api/auth/google/route.ts index c574057..a683074 100644 --- a/src/app/api/auth/google/route.ts +++ b/src/app/api/auth/google/route.ts @@ -92,8 +92,11 @@ export async function POST(request: Request) { }, }) + const isSecureRequest = request.headers.get('x-forwarded-proto') === 'https' + || new URL(request.url).protocol === 'https:' + response.cookies.set('mc-session', token, { - ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000) }), + ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }), }) return response diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 2da7472..57a5274 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -41,8 +41,11 @@ export async function POST(request: Request) { }, }) + const isSecureRequest = request.headers.get('x-forwarded-proto') === 'https' + || new URL(request.url).protocol === 'https:' + response.cookies.set('mc-session', token, { - ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000) }), + ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }), }) return response diff --git a/src/components/dashboard/agent-network.tsx b/src/components/dashboard/agent-network.tsx index 13ee8a9..7628ac7 100644 --- a/src/components/dashboard/agent-network.tsx +++ b/src/components/dashboard/agent-network.tsx @@ -86,7 +86,7 @@ function AgentNode({ data }: { data: any }) {
- {agent.model.split('/').pop()} + {(typeof agent.model === 'string' ? agent.model : '').split('/').pop() || 'unknown'}
{agent.session && ( diff --git a/src/lib/session-cookie.ts b/src/lib/session-cookie.ts index 5363668..508e432 100644 --- a/src/lib/session-cookie.ts +++ b/src/lib/session-cookie.ts @@ -9,9 +9,11 @@ function envFlag(name: string): boolean | undefined { return undefined } -export function getMcSessionCookieOptions(input: { maxAgeSeconds: number }): Partial { +export function getMcSessionCookieOptions(input: { maxAgeSeconds: number; isSecureRequest?: boolean }): Partial { const secureEnv = envFlag('MC_COOKIE_SECURE') - const secure = secureEnv ?? process.env.NODE_ENV === 'production' + // Explicit env wins. Otherwise auto-detect: only set secure if request came over HTTPS. + // Falls back to NODE_ENV=production when no request hint is available. + const secure = secureEnv ?? input.isSecureRequest ?? process.env.NODE_ENV === 'production' // Strict is safest for this app (same-site UI + API), but allow override for edge cases. const sameSiteRaw = (process.env.MC_COOKIE_SAMESITE || 'strict').toLowerCase() diff --git a/src/lib/sessions.ts b/src/lib/sessions.ts index 91165ba..8983e6e 100644 --- a/src/lib/sessions.ts +++ b/src/lib/sessions.ts @@ -62,7 +62,7 @@ export function getAllGatewaySessions(activeWithinMs = 60 * 60 * 1000): GatewayS updatedAt, chatType: s.chatType || 'unknown', channel: s.deliveryContext?.channel || s.lastChannel || s.channel || '', - model: s.model || '', + model: typeof s.model === 'object' && s.model?.primary ? String(s.model.primary) : String(s.model || ''), totalTokens: s.totalTokens || 0, inputTokens: s.inputTokens || 0, outputTokens: s.outputTokens || 0, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 57a6d77..67f9498 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -59,6 +59,13 @@ export function getStatusBadgeColor(status: AgentStatus['status']): string { } } +/** Normalize model field — OpenClaw 2026.3.x may send {primary: "model-name"} instead of a string */ +export function normalizeModel(model: unknown): string { + if (typeof model === 'string') return model + if (model && typeof model === 'object' && 'primary' in model) return String((model as any).primary) + return '' +} + export function sessionToAgent(session: Session): Agent { const getStatusFromSession = (session: Session): AgentStatus['status'] => { if (session.age === 'just now' || session.age.includes('m ago')) return 'active' diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts index 53b86d1..7acb8fa 100644 --- a/src/lib/websocket.ts +++ b/src/lib/websocket.ts @@ -2,6 +2,7 @@ import { useCallback, useRef, useEffect } from 'react' import { useMissionControl } from '@/store' +import { normalizeModel } from '@/lib/utils' // Gateway protocol version (v3 required by OpenClaw 2026.x) const PROTOCOL_VERSION = 3 @@ -172,7 +173,7 @@ export function useWebSocket() { key: session.key || '', kind: session.kind || 'unknown', age: session.age || '', - model: session.model || '', + model: normalizeModel(session.model), tokens: session.tokens || '', flags: session.flags || [], active: session.active || false, @@ -219,7 +220,7 @@ export function useWebSocket() { // Handle various gateway events if (message.data?.type === 'token_usage') { addTokenUsage({ - model: message.data.model, + model: normalizeModel(message.data.model), sessionId: message.data.sessionId, date: new Date().toISOString(), inputTokens: message.data.inputTokens || 0, @@ -291,7 +292,7 @@ export function useWebSocket() { key: session.key || '', kind: session.kind || 'unknown', age: formatAge(session.updatedAt), - model: session.model || '', + model: normalizeModel(session.model), tokens: `${session.totalTokens || 0}/${session.contextTokens || 35000}`, flags: [], active: isActive(session.updatedAt),