nyk commited on
Commit
f5d72bc
·
unverified ·
1 Parent(s): bfb8731

fix: healthcheck auth, secure cookie auto-detect, model object crash (#84)

Browse files

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 CHANGED
@@ -29,5 +29,5 @@ EXPOSE 3000
29
  ENV PORT=3000
30
  ENV HOSTNAME=0.0.0.0
31
  HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
32
- CMD curl -f http://localhost:3000/api/status || exit 1
33
  CMD ["node", "server.js"]
 
29
  ENV PORT=3000
30
  ENV HOSTNAME=0.0.0.0
31
  HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
32
+ CMD curl -f http://localhost:3000/login || exit 1
33
  CMD ["node", "server.js"]
src/app/api/auth/google/route.ts CHANGED
@@ -92,8 +92,11 @@ export async function POST(request: Request) {
92
  },
93
  })
94
 
 
 
 
95
  response.cookies.set('mc-session', token, {
96
- ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000) }),
97
  })
98
 
99
  return response
 
92
  },
93
  })
94
 
95
+ const isSecureRequest = request.headers.get('x-forwarded-proto') === 'https'
96
+ || new URL(request.url).protocol === 'https:'
97
+
98
  response.cookies.set('mc-session', token, {
99
+ ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
100
  })
101
 
102
  return response
src/app/api/auth/login/route.ts CHANGED
@@ -41,8 +41,11 @@ export async function POST(request: Request) {
41
  },
42
  })
43
 
 
 
 
44
  response.cookies.set('mc-session', token, {
45
- ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000) }),
46
  })
47
 
48
  return response
 
41
  },
42
  })
43
 
44
+ const isSecureRequest = request.headers.get('x-forwarded-proto') === 'https'
45
+ || new URL(request.url).protocol === 'https:'
46
+
47
  response.cookies.set('mc-session', token, {
48
+ ...getMcSessionCookieOptions({ maxAgeSeconds: expiresAt - Math.floor(Date.now() / 1000), isSecureRequest }),
49
  })
50
 
51
  return response
src/components/dashboard/agent-network.tsx CHANGED
@@ -86,7 +86,7 @@ function AgentNode({ data }: { data: any }) {
86
  </div>
87
 
88
  <div className="text-xs text-muted-foreground truncate">
89
- {agent.model.split('/').pop()}
90
  </div>
91
 
92
  {agent.session && (
 
86
  </div>
87
 
88
  <div className="text-xs text-muted-foreground truncate">
89
+ {(typeof agent.model === 'string' ? agent.model : '').split('/').pop() || 'unknown'}
90
  </div>
91
 
92
  {agent.session && (
src/lib/session-cookie.ts CHANGED
@@ -9,9 +9,11 @@ function envFlag(name: string): boolean | undefined {
9
  return undefined
10
  }
11
 
12
- export function getMcSessionCookieOptions(input: { maxAgeSeconds: number }): Partial<ResponseCookie> {
13
  const secureEnv = envFlag('MC_COOKIE_SECURE')
14
- const secure = secureEnv ?? process.env.NODE_ENV === 'production'
 
 
15
 
16
  // Strict is safest for this app (same-site UI + API), but allow override for edge cases.
17
  const sameSiteRaw = (process.env.MC_COOKIE_SAMESITE || 'strict').toLowerCase()
 
9
  return undefined
10
  }
11
 
12
+ export function getMcSessionCookieOptions(input: { maxAgeSeconds: number; isSecureRequest?: boolean }): Partial<ResponseCookie> {
13
  const secureEnv = envFlag('MC_COOKIE_SECURE')
14
+ // Explicit env wins. Otherwise auto-detect: only set secure if request came over HTTPS.
15
+ // Falls back to NODE_ENV=production when no request hint is available.
16
+ const secure = secureEnv ?? input.isSecureRequest ?? process.env.NODE_ENV === 'production'
17
 
18
  // Strict is safest for this app (same-site UI + API), but allow override for edge cases.
19
  const sameSiteRaw = (process.env.MC_COOKIE_SAMESITE || 'strict').toLowerCase()
src/lib/sessions.ts CHANGED
@@ -62,7 +62,7 @@ export function getAllGatewaySessions(activeWithinMs = 60 * 60 * 1000): GatewayS
62
  updatedAt,
63
  chatType: s.chatType || 'unknown',
64
  channel: s.deliveryContext?.channel || s.lastChannel || s.channel || '',
65
- model: s.model || '',
66
  totalTokens: s.totalTokens || 0,
67
  inputTokens: s.inputTokens || 0,
68
  outputTokens: s.outputTokens || 0,
 
62
  updatedAt,
63
  chatType: s.chatType || 'unknown',
64
  channel: s.deliveryContext?.channel || s.lastChannel || s.channel || '',
65
+ model: typeof s.model === 'object' && s.model?.primary ? String(s.model.primary) : String(s.model || ''),
66
  totalTokens: s.totalTokens || 0,
67
  inputTokens: s.inputTokens || 0,
68
  outputTokens: s.outputTokens || 0,
src/lib/utils.ts CHANGED
@@ -59,6 +59,13 @@ export function getStatusBadgeColor(status: AgentStatus['status']): string {
59
  }
60
  }
61
 
 
 
 
 
 
 
 
62
  export function sessionToAgent(session: Session): Agent {
63
  const getStatusFromSession = (session: Session): AgentStatus['status'] => {
64
  if (session.age === 'just now' || session.age.includes('m ago')) return 'active'
 
59
  }
60
  }
61
 
62
+ /** Normalize model field — OpenClaw 2026.3.x may send {primary: "model-name"} instead of a string */
63
+ export function normalizeModel(model: unknown): string {
64
+ if (typeof model === 'string') return model
65
+ if (model && typeof model === 'object' && 'primary' in model) return String((model as any).primary)
66
+ return ''
67
+ }
68
+
69
  export function sessionToAgent(session: Session): Agent {
70
  const getStatusFromSession = (session: Session): AgentStatus['status'] => {
71
  if (session.age === 'just now' || session.age.includes('m ago')) return 'active'
src/lib/websocket.ts CHANGED
@@ -2,6 +2,7 @@
2
 
3
  import { useCallback, useRef, useEffect } from 'react'
4
  import { useMissionControl } from '@/store'
 
5
 
6
  // Gateway protocol version (v3 required by OpenClaw 2026.x)
7
  const PROTOCOL_VERSION = 3
@@ -172,7 +173,7 @@ export function useWebSocket() {
172
  key: session.key || '',
173
  kind: session.kind || 'unknown',
174
  age: session.age || '',
175
- model: session.model || '',
176
  tokens: session.tokens || '',
177
  flags: session.flags || [],
178
  active: session.active || false,
@@ -219,7 +220,7 @@ export function useWebSocket() {
219
  // Handle various gateway events
220
  if (message.data?.type === 'token_usage') {
221
  addTokenUsage({
222
- model: message.data.model,
223
  sessionId: message.data.sessionId,
224
  date: new Date().toISOString(),
225
  inputTokens: message.data.inputTokens || 0,
@@ -291,7 +292,7 @@ export function useWebSocket() {
291
  key: session.key || '',
292
  kind: session.kind || 'unknown',
293
  age: formatAge(session.updatedAt),
294
- model: session.model || '',
295
  tokens: `${session.totalTokens || 0}/${session.contextTokens || 35000}`,
296
  flags: [],
297
  active: isActive(session.updatedAt),
 
2
 
3
  import { useCallback, useRef, useEffect } from 'react'
4
  import { useMissionControl } from '@/store'
5
+ import { normalizeModel } from '@/lib/utils'
6
 
7
  // Gateway protocol version (v3 required by OpenClaw 2026.x)
8
  const PROTOCOL_VERSION = 3
 
173
  key: session.key || '',
174
  kind: session.kind || 'unknown',
175
  age: session.age || '',
176
+ model: normalizeModel(session.model),
177
  tokens: session.tokens || '',
178
  flags: session.flags || [],
179
  active: session.active || false,
 
220
  // Handle various gateway events
221
  if (message.data?.type === 'token_usage') {
222
  addTokenUsage({
223
+ model: normalizeModel(message.data.model),
224
  sessionId: message.data.sessionId,
225
  date: new Date().toISOString(),
226
  inputTokens: message.data.inputTokens || 0,
 
292
  key: session.key || '',
293
  kind: session.kind || 'unknown',
294
  age: formatAge(session.updatedAt),
295
+ model: normalizeModel(session.model),
296
  tokens: `${session.totalTokens || 0}/${session.contextTokens || 35000}`,
297
  flags: [],
298
  active: isActive(session.updatedAt),