From 8c0e9b193107735b14c04a2b700c00b422307d3f Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 20:02:48 +0000 Subject: [PATCH 01/13] Add Dockerfiles and compose quickstart --- .env.example | 23 ++++++++++++++++ backend/Dockerfile | 44 +++++++++++++++++++++++++++++ compose.yml | 67 +++++++++++++++++++++++++++++++++++++++++++++ frontend/Dockerfile | 38 +++++++++++++++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 .env.example create mode 100644 backend/Dockerfile create mode 100644 compose.yml create mode 100644 frontend/Dockerfile diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d24078a --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Root compose defaults (safe for local self-host / dev) +# Copy to .env to override. + +# --- app ports (host) --- +FRONTEND_PORT=3000 +BACKEND_PORT=8000 + +# --- database --- +POSTGRES_DB=openclaw_agency +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_PORT=5432 + +# --- redis --- +REDIS_PORT=6379 + +# --- backend settings (see backend/.env.example for full list) --- +CORS_ORIGINS=http://localhost:3000 +DB_AUTO_MIGRATE=true + +# --- frontend settings --- +# Public URL used by the browser to reach the API +NEXT_PUBLIC_API_URL=http://localhost:8000 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..df5a587 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,44 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.12-slim AS base + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +# System deps (keep minimal) +RUN apt-get update \ + && apt-get install -y --no-install-recommends curl ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# Install uv (https://github.com/astral-sh/uv) +RUN curl -LsSf https://astral.sh/uv/install.sh | sh +ENV PATH="/root/.local/bin:${PATH}" + +# --- deps layer --- +FROM base AS deps + +# Copy only dependency metadata first for better build caching +COPY pyproject.toml uv.lock ./ + +# Create venv and sync deps (including runtime) +RUN uv sync --frozen --no-dev + +# --- runtime --- +FROM base AS runtime + +# Copy virtual environment from deps stage +COPY --from=deps /app/.venv /app/.venv +ENV PATH="/app/.venv/bin:${PATH}" + +# Copy app source +COPY alembic ./alembic +COPY alembic.ini ./alembic.ini +COPY app ./app + +# Default API port +EXPOSE 8000 + +# Run the API +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..f6a43a9 --- /dev/null +++ b/compose.yml @@ -0,0 +1,67 @@ +name: openclaw-mission-control + +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_DB: ${POSTGRES_DB:-openclaw_agency} + POSTGRES_USER: ${POSTGRES_USER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres} + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "${POSTGRES_PORT:-5432}:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 5s + timeout: 3s + retries: 20 + + redis: + image: redis:7-alpine + volumes: + - redis_data:/data + ports: + - "${REDIS_PORT:-6379}:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 20 + + backend: + build: + context: ./backend + env_file: + - ./backend/.env.example + environment: + # Override localhost defaults for container networking + DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-openclaw_agency} + REDIS_URL: redis://redis:6379/0 + CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:3000} + DB_AUTO_MIGRATE: ${DB_AUTO_MIGRATE:-true} + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + ports: + - "${BACKEND_PORT:-8000}:8000" + + frontend: + build: + context: ./frontend + args: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + env_file: + - ./frontend/.env.example + environment: + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:8000} + depends_on: + - backend + ports: + - "${FRONTEND_PORT:-3000}:3000" + +volumes: + postgres_data: + redis_data: diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..4a87dcd --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,38 @@ +# syntax=docker/dockerfile:1 + +FROM node:20-alpine AS deps +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +FROM node:20-alpine AS builder +WORKDIR /app + +COPY --from=deps /app/node_modules ./node_modules +COPY . ./ + +# Allows configuring the API URL at build time. +ARG NEXT_PUBLIC_API_URL=http://localhost:8000 +ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} + +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# If provided at runtime, Next will expose NEXT_PUBLIC_* to the browser as well +# (but note some values may be baked at build time). +ENV NEXT_PUBLIC_API_URL=http://localhost:8000 + +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/next.config.ts ./next.config.ts + +EXPOSE 3000 + +CMD ["npm", "run", "start"] From 5682460e700fc9e1e6375ccca46a872c207db218 Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 20:56:01 +0000 Subject: [PATCH 02/13] ci: add GitHub Actions workflow for PR validation --- .github/workflows/ci.yml | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4f17240 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI + +on: + pull_request: + push: + branches: [master] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: python -m pip install --upgrade pip uv + + - name: Cache uv + uses: actions/cache@v4 + with: + path: | + ~/.cache/uv + backend/.venv + key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }} + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: make setup + + - name: Run checks + run: make check From eec790455f1d0dfb01e66c97ebf0853754d3d816 Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 22:31:40 +0000 Subject: [PATCH 03/13] ci: set dummy Clerk key for Next build in CI --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f17240..9c146b2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,4 +47,10 @@ jobs: run: make setup - name: Run checks + env: + # Keep CI builds deterministic and secretless. + NEXT_TELEMETRY_DISABLED: "1" + # Clerk is required at Next build/prerender time in this repo. + # Use a dummy publishable key so forks/PRs can still validate. + NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "pk_test_00000000000000000000000000000000" run: make check From 9f0dd01b651f4ac4ea5e02786c68e00da01ef325 Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 22:35:17 +0000 Subject: [PATCH 04/13] frontend: make Clerk optional during build when key missing/invalid --- frontend/src/app/layout.tsx | 18 +++++++-------- .../src/components/providers/AuthProvider.tsx | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/providers/AuthProvider.tsx diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 146d8a1..905dfae 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -3,9 +3,9 @@ import "./globals.css"; import type { Metadata } from "next"; import type { ReactNode } from "react"; -import { ClerkProvider } from "@clerk/nextjs"; import { IBM_Plex_Sans, Sora } from "next/font/google"; +import { AuthProvider } from "@/components/providers/AuthProvider"; import { QueryProvider } from "@/components/providers/QueryProvider"; export const metadata: Metadata = { @@ -29,14 +29,14 @@ const headingFont = Sora({ export default function RootLayout({ children }: { children: ReactNode }) { return ( - - - + + + {children} - - - + + + ); } diff --git a/frontend/src/components/providers/AuthProvider.tsx b/frontend/src/components/providers/AuthProvider.tsx new file mode 100644 index 0000000..03ceb00 --- /dev/null +++ b/frontend/src/components/providers/AuthProvider.tsx @@ -0,0 +1,22 @@ +"use client"; + +import { ClerkProvider } from "@clerk/nextjs"; +import type { ReactNode } from "react"; + +function isLikelyValidClerkPublishableKey(key: string | undefined): key is string { + if (!key) return false; + // Clerk publishable keys look like: pk_test_... or pk_live_... + // In CI we want builds to stay secretless; if the key isn't present/valid, + // we skip Clerk entirely so `next build` can prerender. + return /^pk_(test|live)_[A-Za-z0-9]+$/.test(key); +} + +export function AuthProvider({ children }: { children: ReactNode }) { + const publishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + + if (!isLikelyValidClerkPublishableKey(publishableKey)) { + return <>{children}; + } + + return {children}; +} From b88936ae9cf3edc47be67865d5361da829a62db7 Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 22:42:03 +0000 Subject: [PATCH 05/13] ci: remove dummy Clerk key; tighten AuthProvider guard --- .github/workflows/ci.yml | 3 --- frontend/src/components/providers/AuthProvider.tsx | 10 +++++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9c146b2..9ff7d43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,4 @@ jobs: env: # Keep CI builds deterministic and secretless. NEXT_TELEMETRY_DISABLED: "1" - # Clerk is required at Next build/prerender time in this repo. - # Use a dummy publishable key so forks/PRs can still validate. - NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: "pk_test_00000000000000000000000000000000" run: make check diff --git a/frontend/src/components/providers/AuthProvider.tsx b/frontend/src/components/providers/AuthProvider.tsx index 03ceb00..4da743e 100644 --- a/frontend/src/components/providers/AuthProvider.tsx +++ b/frontend/src/components/providers/AuthProvider.tsx @@ -8,7 +8,15 @@ function isLikelyValidClerkPublishableKey(key: string | undefined): key is strin // Clerk publishable keys look like: pk_test_... or pk_live_... // In CI we want builds to stay secretless; if the key isn't present/valid, // we skip Clerk entirely so `next build` can prerender. - return /^pk_(test|live)_[A-Za-z0-9]+$/.test(key); + // + // Note: Clerk appears to validate key *contents*, not just shape. We therefore + // use a conservative heuristic to avoid treating obvious placeholders as valid. + const m = /^pk_(test|live)_([A-Za-z0-9]+)$/.exec(key); + if (!m) return false; + const body = m[2]; + if (body.length < 16) return false; + if (/^0+$/.test(body)) return false; + return true; } export function AuthProvider({ children }: { children: ReactNode }) { From 6de49310bcb46379e84b1661555650894f012ded Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 22:49:54 +0000 Subject: [PATCH 06/13] frontend: provide Clerk-safe wrappers for secretless CI prerender --- .../src/app/agents/[agentId]/edit/page.tsx | 2 +- frontend/src/app/agents/[agentId]/page.tsx | 2 +- frontend/src/app/agents/new/page.tsx | 2 +- frontend/src/app/agents/page.tsx | 2 +- .../app/boards/[boardId]/approvals/page.tsx | 2 +- .../src/app/boards/[boardId]/edit/page.tsx | 2 +- frontend/src/app/boards/[boardId]/page.tsx | 2 +- frontend/src/app/boards/new/page.tsx | 2 +- frontend/src/app/boards/page.tsx | 2 +- frontend/src/app/dashboard/page.tsx | 2 +- .../app/gateways/[gatewayId]/edit/page.tsx | 2 +- .../src/app/gateways/[gatewayId]/page.tsx | 2 +- frontend/src/app/gateways/new/page.tsx | 2 +- frontend/src/app/gateways/page.tsx | 2 +- frontend/src/app/onboarding/page.tsx | 2 +- frontend/src/auth/clerk.tsx | 74 +++++++++++++++++++ .../src/components/BoardApprovalsPanel.tsx | 2 +- .../src/components/organisms/LandingHero.tsx | 2 +- .../src/components/organisms/UserMenu.tsx | 2 +- .../components/templates/DashboardShell.tsx | 2 +- .../src/components/templates/LandingShell.tsx | 2 +- frontend/src/proxy.ts | 14 +++- 22 files changed, 107 insertions(+), 21 deletions(-) create mode 100644 frontend/src/auth/clerk.tsx diff --git a/frontend/src/app/agents/[agentId]/edit/page.tsx b/frontend/src/app/agents/[agentId]/edit/page.tsx index 377a775..36bbc35 100644 --- a/frontend/src/app/agents/[agentId]/edit/page.tsx +++ b/frontend/src/app/agents/[agentId]/edit/page.tsx @@ -3,7 +3,7 @@ import { useMemo, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { diff --git a/frontend/src/app/agents/[agentId]/page.tsx b/frontend/src/app/agents/[agentId]/page.tsx index 29706e3..cfe0731 100644 --- a/frontend/src/app/agents/[agentId]/page.tsx +++ b/frontend/src/app/agents/[agentId]/page.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { diff --git a/frontend/src/app/agents/new/page.tsx b/frontend/src/app/agents/new/page.tsx index 92fec06..69c0378 100644 --- a/frontend/src/app/agents/new/page.tsx +++ b/frontend/src/app/agents/new/page.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { diff --git a/frontend/src/app/agents/page.tsx b/frontend/src/app/agents/page.tsx index ea5cb45..e718e27 100644 --- a/frontend/src/app/agents/page.tsx +++ b/frontend/src/app/agents/page.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { type ColumnDef, type SortingState, diff --git a/frontend/src/app/boards/[boardId]/approvals/page.tsx b/frontend/src/app/boards/[boardId]/approvals/page.tsx index a362a64..1bf60bd 100644 --- a/frontend/src/app/boards/[boardId]/approvals/page.tsx +++ b/frontend/src/app/boards/[boardId]/approvals/page.tsx @@ -2,7 +2,7 @@ import { useParams } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut } from "@/auth/clerk"; import { BoardApprovalsPanel } from "@/components/BoardApprovalsPanel"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index a3f9813..7f147ac 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { X } from "lucide-react"; import { ApiError } from "@/api/mutator"; diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index 11a24cc..c5bfcd8 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -3,7 +3,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { Activity, ArrowUpRight, diff --git a/frontend/src/app/boards/new/page.tsx b/frontend/src/app/boards/new/page.tsx index c9fb9ae..2d851d3 100644 --- a/frontend/src/app/boards/new/page.tsx +++ b/frontend/src/app/boards/new/page.tsx @@ -4,7 +4,7 @@ import { useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { useCreateBoardApiV1BoardsPost } from "@/api/generated/boards/boards"; diff --git a/frontend/src/app/boards/page.tsx b/frontend/src/app/boards/page.tsx index 2234ef8..b4c96a6 100644 --- a/frontend/src/app/boards/page.tsx +++ b/frontend/src/app/boards/page.tsx @@ -3,7 +3,7 @@ import { useMemo, useState } from "react"; import Link from "next/link"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { type ColumnDef, flexRender, diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index be3ef59..f32dfce 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { Area, AreaChart, diff --git a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx index 9cd78a8..3304679 100644 --- a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react"; import { ApiError } from "@/api/mutator"; diff --git a/frontend/src/app/gateways/[gatewayId]/page.tsx b/frontend/src/app/gateways/[gatewayId]/page.tsx index a8f4133..3aad93b 100644 --- a/frontend/src/app/gateways/[gatewayId]/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/page.tsx @@ -3,7 +3,7 @@ import { useMemo } from "react"; import { useParams, useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { ApiError } from "@/api/mutator"; import { diff --git a/frontend/src/app/gateways/new/page.tsx b/frontend/src/app/gateways/new/page.tsx index f310fd7..dea970c 100644 --- a/frontend/src/app/gateways/new/page.tsx +++ b/frontend/src/app/gateways/new/page.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { CheckCircle2, RefreshCcw, XCircle } from "lucide-react"; import { ApiError } from "@/api/mutator"; diff --git a/frontend/src/app/gateways/page.tsx b/frontend/src/app/gateways/page.tsx index ca5ac18..b60c0ad 100644 --- a/frontend/src/app/gateways/page.tsx +++ b/frontend/src/app/gateways/page.tsx @@ -3,7 +3,7 @@ import { useMemo, useState } from "react"; import Link from "next/link"; -import { SignInButton, SignedIn, SignedOut, useAuth } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; import { type ColumnDef, type SortingState, diff --git a/frontend/src/app/onboarding/page.tsx b/frontend/src/app/onboarding/page.tsx index 21bd936..24a934c 100644 --- a/frontend/src/app/onboarding/page.tsx +++ b/frontend/src/app/onboarding/page.tsx @@ -3,7 +3,7 @@ import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; -import { SignInButton, SignedIn, SignedOut, useAuth, useUser } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut, useAuth, useUser } from "@/auth/clerk"; import { Globe, Info, RotateCcw, Save, User } from "lucide-react"; import { ApiError } from "@/api/mutator"; diff --git a/frontend/src/auth/clerk.tsx b/frontend/src/auth/clerk.tsx new file mode 100644 index 0000000..d5ee1dc --- /dev/null +++ b/frontend/src/auth/clerk.tsx @@ -0,0 +1,74 @@ +"use client"; + +import type { ReactNode } from "react"; + +// NOTE: We intentionally keep this file very small and dependency-free. +// It provides CI/secretless-build safe fallbacks for Clerk hooks/components. + +import { + ClerkProvider, + SignedIn as ClerkSignedIn, + SignedOut as ClerkSignedOut, + SignInButton as ClerkSignInButton, + SignOutButton as ClerkSignOutButton, + useAuth as clerkUseAuth, + useUser as clerkUseUser, +} from "@clerk/nextjs"; + +export function isClerkEnabled(): boolean { + const key = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + if (!key) return false; + + // Clerk validates publishable key contents at runtime; use a conservative heuristic. + const m = /^pk_(test|live)_([A-Za-z0-9]+)$/.exec(key); + if (!m) return false; + const body = m[2]; + if (body.length < 16) return false; + if (/^0+$/.test(body)) return false; + return true; +} + +export function SignedIn(props: { children: ReactNode }) { + if (!isClerkEnabled()) return null; + return {props.children}; +} + +export function SignedOut(props: { children: ReactNode }) { + if (!isClerkEnabled()) return <>{props.children}; + return {props.children}; +} + +// Accept arbitrary Clerk component props so existing call sites don't need edits. +export function SignInButton(props: any) { + if (!isClerkEnabled()) return null; + return ; +} + +export function SignOutButton(props: any) { + if (!isClerkEnabled()) return null; + return ; +} + +export function useUser() { + if (!isClerkEnabled()) { + return { isLoaded: true, isSignedIn: false, user: null } as const; + } + return clerkUseUser(); +} + +export function useAuth() { + if (!isClerkEnabled()) { + return { + isLoaded: true, + isSignedIn: false, + userId: null, + sessionId: null, + getToken: async () => null, + } as const; + } + return clerkUseAuth(); +} + +// Re-export ClerkProvider for places that want to mount it, but strongly prefer +// gating via isClerkEnabled() at call sites. +export { ClerkProvider }; diff --git a/frontend/src/components/BoardApprovalsPanel.tsx b/frontend/src/components/BoardApprovalsPanel.tsx index 0ed470d..264d73c 100644 --- a/frontend/src/components/BoardApprovalsPanel.tsx +++ b/frontend/src/components/BoardApprovalsPanel.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from "react"; -import { useAuth } from "@clerk/nextjs"; +import { useAuth } from "@/auth/clerk"; import { useQueryClient } from "@tanstack/react-query"; import { Clock } from "lucide-react"; diff --git a/frontend/src/components/organisms/LandingHero.tsx b/frontend/src/components/organisms/LandingHero.tsx index 38db84f..349bd48 100644 --- a/frontend/src/components/organisms/LandingHero.tsx +++ b/frontend/src/components/organisms/LandingHero.tsx @@ -1,6 +1,6 @@ "use client"; -import { SignInButton, SignedIn, SignedOut } from "@clerk/nextjs"; +import { SignInButton, SignedIn, SignedOut } from "@/auth/clerk"; import { HeroCopy } from "@/components/molecules/HeroCopy"; import { Button } from "@/components/ui/button"; diff --git a/frontend/src/components/organisms/UserMenu.tsx b/frontend/src/components/organisms/UserMenu.tsx index 0918924..661fbea 100644 --- a/frontend/src/components/organisms/UserMenu.tsx +++ b/frontend/src/components/organisms/UserMenu.tsx @@ -1,7 +1,7 @@ "use client"; import Image from "next/image"; -import { SignOutButton, useUser } from "@clerk/nextjs"; +import { SignOutButton, useUser } from "@/auth/clerk"; import { LogOut } from "lucide-react"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; diff --git a/frontend/src/components/templates/DashboardShell.tsx b/frontend/src/components/templates/DashboardShell.tsx index ace048a..04b516a 100644 --- a/frontend/src/components/templates/DashboardShell.tsx +++ b/frontend/src/components/templates/DashboardShell.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; -import { SignedIn, useUser } from "@clerk/nextjs"; +import { SignedIn, useUser } from "@/auth/clerk"; import { BrandMark } from "@/components/atoms/BrandMark"; import { UserMenu } from "@/components/organisms/UserMenu"; diff --git a/frontend/src/components/templates/LandingShell.tsx b/frontend/src/components/templates/LandingShell.tsx index 59760fc..32d6af7 100644 --- a/frontend/src/components/templates/LandingShell.tsx +++ b/frontend/src/components/templates/LandingShell.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; -import { SignedIn } from "@clerk/nextjs"; +import { SignedIn } from "@/auth/clerk"; import { BrandMark } from "@/components/atoms/BrandMark"; import { UserMenu } from "@/components/organisms/UserMenu"; diff --git a/frontend/src/proxy.ts b/frontend/src/proxy.ts index 7543980..41a0eb8 100644 --- a/frontend/src/proxy.ts +++ b/frontend/src/proxy.ts @@ -1,6 +1,18 @@ +import { NextResponse } from "next/server"; import { clerkMiddleware } from "@clerk/nextjs/server"; -export default clerkMiddleware(); +const isClerkEnabled = () => { + const key = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + if (!key) return false; + const m = /^pk_(test|live)_([A-Za-z0-9]+)$/.exec(key); + if (!m) return false; + const body = m[2]; + if (body.length < 16) return false; + if (/^0+$/.test(body)) return false; + return true; +}; + +export default isClerkEnabled() ? clerkMiddleware() : () => NextResponse.next(); export const config = { matcher: [ From e8e27fdcc3b79878df88588fc8d1afc5427cd308 Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 22:51:58 +0000 Subject: [PATCH 07/13] frontend: force-dynamic on auth routes to avoid prerender in CI --- frontend/src/app/agents/[agentId]/edit/page.tsx | 2 ++ frontend/src/app/agents/[agentId]/page.tsx | 2 ++ frontend/src/app/agents/new/page.tsx | 2 ++ frontend/src/app/agents/page.tsx | 2 ++ frontend/src/app/boards/[boardId]/approvals/page.tsx | 2 ++ frontend/src/app/boards/[boardId]/edit/page.tsx | 2 ++ frontend/src/app/boards/[boardId]/page.tsx | 2 ++ frontend/src/app/boards/new/page.tsx | 2 ++ frontend/src/app/boards/page.tsx | 2 ++ frontend/src/app/dashboard/page.tsx | 2 ++ frontend/src/app/gateways/[gatewayId]/edit/page.tsx | 2 ++ frontend/src/app/gateways/[gatewayId]/page.tsx | 2 ++ frontend/src/app/gateways/new/page.tsx | 2 ++ frontend/src/app/gateways/page.tsx | 2 ++ frontend/src/app/onboarding/page.tsx | 2 ++ 15 files changed, 30 insertions(+) diff --git a/frontend/src/app/agents/[agentId]/edit/page.tsx b/frontend/src/app/agents/[agentId]/edit/page.tsx index 36bbc35..408079c 100644 --- a/frontend/src/app/agents/[agentId]/edit/page.tsx +++ b/frontend/src/app/agents/[agentId]/edit/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import { useParams, useRouter } from "next/navigation"; diff --git a/frontend/src/app/agents/[agentId]/page.tsx b/frontend/src/app/agents/[agentId]/page.tsx index cfe0731..76f624d 100644 --- a/frontend/src/app/agents/[agentId]/page.tsx +++ b/frontend/src/app/agents/[agentId]/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; diff --git a/frontend/src/app/agents/new/page.tsx b/frontend/src/app/agents/new/page.tsx index 69c0378..3d08640 100644 --- a/frontend/src/app/agents/new/page.tsx +++ b/frontend/src/app/agents/new/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useState } from "react"; import { useRouter } from "next/navigation"; diff --git a/frontend/src/app/agents/page.tsx b/frontend/src/app/agents/page.tsx index e718e27..3a09a95 100644 --- a/frontend/src/app/agents/page.tsx +++ b/frontend/src/app/agents/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; diff --git a/frontend/src/app/boards/[boardId]/approvals/page.tsx b/frontend/src/app/boards/[boardId]/approvals/page.tsx index 1bf60bd..531ca13 100644 --- a/frontend/src/app/boards/[boardId]/approvals/page.tsx +++ b/frontend/src/app/boards/[boardId]/approvals/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useParams } from "next/navigation"; import { SignInButton, SignedIn, SignedOut } from "@/auth/clerk"; diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index 7f147ac..70b9b37 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter, useSearchParams } from "next/navigation"; diff --git a/frontend/src/app/boards/[boardId]/page.tsx b/frontend/src/app/boards/[boardId]/page.tsx index c5bfcd8..8ee9401 100644 --- a/frontend/src/app/boards/[boardId]/page.tsx +++ b/frontend/src/app/boards/[boardId]/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useParams, useRouter } from "next/navigation"; diff --git a/frontend/src/app/boards/new/page.tsx b/frontend/src/app/boards/new/page.tsx index 2d851d3..5b188ec 100644 --- a/frontend/src/app/boards/new/page.tsx +++ b/frontend/src/app/boards/new/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; diff --git a/frontend/src/app/boards/page.tsx b/frontend/src/app/boards/page.tsx index b4c96a6..7e979e8 100644 --- a/frontend/src/app/boards/page.tsx +++ b/frontend/src/app/boards/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index f32dfce..b21b5f1 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo } from "react"; import { SignInButton, SignedIn, SignedOut, useAuth } from "@/auth/clerk"; diff --git a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx index 3304679..8f1233b 100644 --- a/frontend/src/app/gateways/[gatewayId]/edit/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/edit/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useState } from "react"; import { useParams, useRouter } from "next/navigation"; diff --git a/frontend/src/app/gateways/[gatewayId]/page.tsx b/frontend/src/app/gateways/[gatewayId]/page.tsx index 3aad93b..726d4a1 100644 --- a/frontend/src/app/gateways/[gatewayId]/page.tsx +++ b/frontend/src/app/gateways/[gatewayId]/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo } from "react"; import { useParams, useRouter } from "next/navigation"; diff --git a/frontend/src/app/gateways/new/page.tsx b/frontend/src/app/gateways/new/page.tsx index dea970c..1c99568 100644 --- a/frontend/src/app/gateways/new/page.tsx +++ b/frontend/src/app/gateways/new/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useState } from "react"; import { useRouter } from "next/navigation"; diff --git a/frontend/src/app/gateways/page.tsx b/frontend/src/app/gateways/page.tsx index b60c0ad..5ede20a 100644 --- a/frontend/src/app/gateways/page.tsx +++ b/frontend/src/app/gateways/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useMemo, useState } from "react"; import Link from "next/link"; diff --git a/frontend/src/app/onboarding/page.tsx b/frontend/src/app/onboarding/page.tsx index 24a934c..66613ef 100644 --- a/frontend/src/app/onboarding/page.tsx +++ b/frontend/src/app/onboarding/page.tsx @@ -1,5 +1,7 @@ "use client"; +export const dynamic = "force-dynamic"; + import { useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; From 527879b55e244e8823e33b96357bfbaf7d911144 Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 22:55:27 +0000 Subject: [PATCH 08/13] ci: add workflow_dispatch for manual reruns --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ff7d43..7401f9e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,8 @@ on: pull_request: push: branches: [master] + # Allow maintainers to manually kick CI when GitHub doesn't create a run for a new head SHA. + workflow_dispatch: concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} From 39d2e530a71667ded18ae85a09388a49d3e28101 Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 22:56:07 +0000 Subject: [PATCH 09/13] frontend: disable Clerk only when publishable key is absent --- frontend/src/auth/clerk.tsx | 14 ++++---------- frontend/src/proxy.ts | 11 +---------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/frontend/src/auth/clerk.tsx b/frontend/src/auth/clerk.tsx index d5ee1dc..5dfcd62 100644 --- a/frontend/src/auth/clerk.tsx +++ b/frontend/src/auth/clerk.tsx @@ -16,16 +16,10 @@ import { } from "@clerk/nextjs"; export function isClerkEnabled(): boolean { - const key = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; - if (!key) return false; - - // Clerk validates publishable key contents at runtime; use a conservative heuristic. - const m = /^pk_(test|live)_([A-Za-z0-9]+)$/.exec(key); - if (!m) return false; - const body = m[2]; - if (body.length < 16) return false; - if (/^0+$/.test(body)) return false; - return true; + // Invariant: Clerk is disabled ONLY when the publishable key is absent. + // If a key is present, we assume Clerk is intended to be enabled and we let + // Clerk fail fast if the key is invalid/misconfigured. + return Boolean(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY); } export function SignedIn(props: { children: ReactNode }) { diff --git a/frontend/src/proxy.ts b/frontend/src/proxy.ts index 41a0eb8..75b3086 100644 --- a/frontend/src/proxy.ts +++ b/frontend/src/proxy.ts @@ -1,16 +1,7 @@ import { NextResponse } from "next/server"; import { clerkMiddleware } from "@clerk/nextjs/server"; -const isClerkEnabled = () => { - const key = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; - if (!key) return false; - const m = /^pk_(test|live)_([A-Za-z0-9]+)$/.exec(key); - if (!m) return false; - const body = m[2]; - if (body.length < 16) return false; - if (/^0+$/.test(body)) return false; - return true; -}; +const isClerkEnabled = () => Boolean(process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY); export default isClerkEnabled() ? clerkMiddleware() : () => NextResponse.next(); From ba21ca9b6184c7aa0569a50ab5016af296ae3d33 Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 22:57:43 +0000 Subject: [PATCH 10/13] chore: noop commit to retrigger CI --- .github/workflows/.noop | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/workflows/.noop diff --git a/.github/workflows/.noop b/.github/workflows/.noop new file mode 100644 index 0000000..6fb76ed --- /dev/null +++ b/.github/workflows/.noop @@ -0,0 +1,2 @@ + +# noop: retrigger actions 2026-02-06T22:57:43Z From 720c19f85277c7ec75c2644a695ecf35eef5b3a9 Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 22:59:21 +0000 Subject: [PATCH 11/13] frontend: type Clerk button wrappers (no any) --- frontend/src/auth/clerk.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/auth/clerk.tsx b/frontend/src/auth/clerk.tsx index 5dfcd62..a61e8c9 100644 --- a/frontend/src/auth/clerk.tsx +++ b/frontend/src/auth/clerk.tsx @@ -15,6 +15,11 @@ import { useUser as clerkUseUser, } from "@clerk/nextjs"; +import type { + SignInButtonProps, + SignOutButtonProps, +} from "@clerk/nextjs"; + export function isClerkEnabled(): boolean { // Invariant: Clerk is disabled ONLY when the publishable key is absent. // If a key is present, we assume Clerk is intended to be enabled and we let @@ -32,13 +37,13 @@ export function SignedOut(props: { children: ReactNode }) { return {props.children}; } -// Accept arbitrary Clerk component props so existing call sites don't need edits. -export function SignInButton(props: any) { +// Keep the same prop surface as Clerk components so call sites don't need edits. +export function SignInButton(props: SignInButtonProps) { if (!isClerkEnabled()) return null; return ; } -export function SignOutButton(props: any) { +export function SignOutButton(props: SignOutButtonProps) { if (!isClerkEnabled()) return null; return ; } From 20878c1d41d532caa91a2306e317d8cd4358220f Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 23:02:35 +0000 Subject: [PATCH 12/13] frontend: type Clerk button wrappers via ComponentProps --- frontend/src/auth/clerk.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/frontend/src/auth/clerk.tsx b/frontend/src/auth/clerk.tsx index a61e8c9..691b3a0 100644 --- a/frontend/src/auth/clerk.tsx +++ b/frontend/src/auth/clerk.tsx @@ -15,10 +15,7 @@ import { useUser as clerkUseUser, } from "@clerk/nextjs"; -import type { - SignInButtonProps, - SignOutButtonProps, -} from "@clerk/nextjs"; +import type { ComponentProps } from "react"; export function isClerkEnabled(): boolean { // Invariant: Clerk is disabled ONLY when the publishable key is absent. @@ -38,12 +35,12 @@ export function SignedOut(props: { children: ReactNode }) { } // Keep the same prop surface as Clerk components so call sites don't need edits. -export function SignInButton(props: SignInButtonProps) { +export function SignInButton(props: ComponentProps) { if (!isClerkEnabled()) return null; return ; } -export function SignOutButton(props: SignOutButtonProps) { +export function SignOutButton(props: ComponentProps) { if (!isClerkEnabled()) return null; return ; } From 24a3a720d320af7f7544a493ce0a329e1e5cf71c Mon Sep 17 00:00:00 2001 From: "Ishaan (OpenClaw)" Date: Fri, 6 Feb 2026 23:22:13 +0000 Subject: [PATCH 13/13] chore: remove noop workflow trigger file --- .github/workflows/.noop | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .github/workflows/.noop diff --git a/.github/workflows/.noop b/.github/workflows/.noop deleted file mode 100644 index 6fb76ed..0000000 --- a/.github/workflows/.noop +++ /dev/null @@ -1,2 +0,0 @@ - -# noop: retrigger actions 2026-02-06T22:57:43Z