From b6f0d5c126391028abdae0a671c73e6be801a154 Mon Sep 17 00:00:00 2001 From: Alfred Date: Wed, 26 Aug 2026 04:21:02 +0000 Subject: [PATCH 1/3] fix(ci): lowercase GHCR image name for buildx tag validation Docker/GHCR require lowercase repository names. github.repository (SabaTech-dev/Alfred-Mission-Control) broke 'docker buildx build' with 'invalid tag: repository name must be lowercase', failing the SLSA Build & Attest job on every push to main. Introduce workflow-level env.IMAGE_NAME (canonical lowercase artifact name) and use it consistently for: build tags, attestation subject-name and the verification summary (which also referenced a never-pushed 'sha-' prefixed tag). --- .github/workflows/slsa-provenance.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/slsa-provenance.yml b/.github/workflows/slsa-provenance.yml index 5733050b..1876b604 100644 --- a/.github/workflows/slsa-provenance.yml +++ b/.github/workflows/slsa-provenance.yml @@ -22,6 +22,12 @@ concurrency: group: slsa-${{ github.ref }} cancel-in-progress: true +# GHCR/Docker exigen repositorios en lowercase; github.repository contiene +# mayúsculas (SabaTech-dev/Alfred-Mission-Control) y buildx rechaza el tag. +# Nombre canónico fijo del artefacto — usar SIEMPRE env.IMAGE_NAME. +env: + IMAGE_NAME: ghcr.io/sabatech-dev/alfred-mission-control + jobs: provenance: name: Build & Attest @@ -48,14 +54,14 @@ jobs: with: context: . push: true - tags: ghcr.io/${{ github.repository }}:${{ github.sha }} + tags: ${{ env.IMAGE_NAME }}:${{ github.sha }} provenance: true sbom: true - name: Generate build provenance attestation uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c with: - subject-name: ghcr.io/${{ github.repository }} + subject-name: ${{ env.IMAGE_NAME }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true @@ -73,5 +79,5 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "**Verification command:**" >> $GITHUB_STEP_SUMMARY echo '```bash' >> $GITHUB_STEP_SUMMARY - echo "docker buildx imagetools inspect ghcr.io/${{ github.repository }}:sha-${{ github.sha }}" >> $GITHUB_STEP_SUMMARY + echo "docker buildx imagetools inspect ${{ env.IMAGE_NAME }}:${{ github.sha }}" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY From 119c2bffe8eb54f7ea7aa9c3d084a0f5e86f7a31 Mon Sep 17 00:00:00 2001 From: Joker Date: Fri, 4 Sep 2026 18:14:57 +0000 Subject: [PATCH 2/3] fix(health): drop stale ollama check, add llama.cpp rerank and embed-memory TCP checks Ollama was retired on 2026-08-28; the running build still probed it and kept the dashboard permanently degraded. This adds TCP checks for the llama.cpp reranker (:8005) and memory embeddings (:8006) that replaced it. --- src/app/(dashboard)/admin/system-health/page.tsx | 2 +- src/app/api/system/monitor/route.ts | 4 +++- src/lib/stack-health.test.ts | 2 +- src/lib/stack-health.ts | 8 +++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/app/(dashboard)/admin/system-health/page.tsx b/src/app/(dashboard)/admin/system-health/page.tsx index 871507b2..7e4bc607 100644 --- a/src/app/(dashboard)/admin/system-health/page.tsx +++ b/src/app/(dashboard)/admin/system-health/page.tsx @@ -22,7 +22,7 @@ const serviceCategories: Record = { "alfred-mc": "core", "openclaw-gateway": "core", "postgresql": "core", - "ollama": "llm", + "llama.cpp-rerank": "llm", "llama.cpp-gpu": "llm", "llama.cpp-embed": "llm", "coolify": "dev", diff --git a/src/app/api/system/monitor/route.ts b/src/app/api/system/monitor/route.ts index 8a1d980b..e8ee3f59 100644 --- a/src/app/api/system/monitor/route.ts +++ b/src/app/api/system/monitor/route.ts @@ -17,7 +17,9 @@ const PROBE_TIMEOUT_MS = 5000; const SYSTEM_SERVICES = [ "alfred-mission-control", "docker", - "ollama", + "llama-main", + "llama-embeddings", + "llama-rerank", "tailscaled", "fail2ban", "redis-server", diff --git a/src/lib/stack-health.test.ts b/src/lib/stack-health.test.ts index b791af9a..50f1bc73 100644 --- a/src/lib/stack-health.test.ts +++ b/src/lib/stack-health.test.ts @@ -15,7 +15,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; const mockChecks = { gateway: { name: "openclaw-gateway", status: "up" as const, details: "OK" }, postgresql: { name: "postgresql", status: "up" as const, details: "OK" }, - ollama: { name: "ollama", status: "up" as const, details: "OK" }, + llamaRerank: { name: "llama.cpp-rerank", status: "up" as const, details: "OK" }, coolify: { name: "coolify", status: "up" as const, details: "OK" }, browserless: { name: "browserless", status: "up" as const, details: "OK" }, langfuse: { name: "langfuse", status: "up" as const, details: "OK" }, diff --git a/src/lib/stack-health.ts b/src/lib/stack-health.ts index 989236e6..becc02b2 100644 --- a/src/lib/stack-health.ts +++ b/src/lib/stack-health.ts @@ -236,16 +236,17 @@ async function checkHttpService(name: string, url: string, port: number): Promis export async function collectStackServiceChecks(): Promise { const dockerContainers = parseDockerContainers(); - const [gateway, postgresql, ollama, coolify, browserless, langfuse, qmd, llamaGpu, llamaEmbed, searxng, engram, prAgent] = await Promise.all([ + const [gateway, postgresql, llamaRerank, coolify, browserless, langfuse, qmd, llamaGpu, llamaEmbed, llamaEmbedMem, searxng, engram, prAgent] = await Promise.all([ checkGatewayService(), checkPostgresService(dockerContainers), - checkTcpService("ollama", 11434), + checkTcpService("llama.cpp-rerank", 8005), checkTcpService("coolify", 8000), checkHttpService("browserless", "http://127.0.0.1:3002/pressure", 3002), checkHttpService("langfuse", "http://127.0.0.1:3001", 3001), checkTcpService("qmd-mcp", 8181), checkTcpService("llama.cpp-gpu", 8001), checkTcpService("llama.cpp-embed", 8002), + checkTcpService("llama.cpp-embed-memory", 8006), checkHttpService("searxng", "http://127.0.0.1:8081", 8081), checkTcpService("engram", 7437), checkTcpService("pr-agent", 3003), @@ -255,13 +256,14 @@ export async function collectStackServiceChecks(): Promise { name: "alfred-mc", status: "up", details: "API route responding" }, gateway, postgresql, - ollama, + llamaRerank, coolify, browserless, langfuse, qmd, llamaGpu, llamaEmbed, + llamaEmbedMem, searxng, engram, prAgent, From abfc033bd0d7aeea288b8e4fc0eac99e998b28ff Mon Sep 17 00:00:00 2001 From: Joker Date: Wed, 9 Sep 2026 14:03:14 +0000 Subject: [PATCH 3/3] fix(health): drop browserless check + report llama.cpp-gpu as HELD (cards 64146d33/64df4def/1e20061c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove browserless :3002 probe (service uninstalled 2026-09-04) — phantom degraded source - llama.cpp-gpu :8001 (Ornith) reported as held (intentional stop Joker 2026-09-08), never down; flips to up automatically if the port returns. Serving prod = 1Cat-vLLM :8009. - Add held status end-to-end: StackServiceCheck type, summarizeStackHealth (held != degraded), formatStackHeartbeat symbols (up ✅ / held ⏸️ / down ❌), dashboard badge (amber) + counter - Category map: drop browserless, add llama.cpp-embed-memory to llm - NOTE doc: ollama removed 28-ago, browserless uninstalled 04-sep, :8001 HELD - Tests: 12 (held semantics + absence guarantees for ollama/browserless) --- .../(dashboard)/admin/system-health/page.tsx | 19 ++++-- src/lib/stack-health.test.ts | 47 +++++++++++-- src/lib/stack-health.ts | 67 ++++++++++++++++--- 3 files changed, 112 insertions(+), 21 deletions(-) diff --git a/src/app/(dashboard)/admin/system-health/page.tsx b/src/app/(dashboard)/admin/system-health/page.tsx index 7e4bc607..337dd45e 100644 --- a/src/app/(dashboard)/admin/system-health/page.tsx +++ b/src/app/(dashboard)/admin/system-health/page.tsx @@ -5,7 +5,7 @@ import { useEffect, useState } from "react"; interface HealthCheck { name: string; - status: "up" | "down"; + status: "up" | "down" | "held"; details: string; } @@ -25,8 +25,8 @@ const serviceCategories: Record = { "llama.cpp-rerank": "llm", "llama.cpp-gpu": "llm", "llama.cpp-embed": "llm", + "llama.cpp-embed-memory": "llm", "coolify": "dev", - "browserless": "services", "langfuse": "services", "searxng": "services", "qmd-mcp": "services", @@ -43,11 +43,15 @@ const categoryLabels: Record = { }; function statusColor(status: string) { - return status === "up" ? "#22c55e" : "#ef4444"; + if (status === "up") return "#22c55e"; + if (status === "held") return "#f59e0b"; + return "#ef4444"; } function statusBg(status: string) { - return status === "up" ? "rgba(34,197,94,0.1)" : "rgba(239,68,68,0.1)"; + if (status === "up") return "rgba(34,197,94,0.1)"; + if (status === "held") return "rgba(245,158,11,0.1)"; + return "rgba(239,68,68,0.1)"; } function ServiceCard({ check }: { check: HealthCheck }) { @@ -136,7 +140,10 @@ export default function SystemHealthPage() { const totalServices = allChecks.length; const upServices = allChecks.filter((c) => c.status === "up").length; - const overallStatus = upServices === totalServices ? "healthy" : upServices > 0 ? "degraded" : "down"; + const heldCount = allChecks.filter((c) => c.status === "held").length; + const downCount = allChecks.filter((c) => c.status === "down").length; + // "held" services are intentional operator stops — they do not degrade health. + const overallStatus = downCount === 0 ? "healthy" : upServices > 0 ? "degraded" : "down"; return ( - {upServices}/{totalServices} services up + {upServices}/{totalServices} services up{heldCount > 0 ? ` · ⏸️ ${heldCount} held` : ""}
diff --git a/src/lib/stack-health.test.ts b/src/lib/stack-health.test.ts index 50f1bc73..1af76a71 100644 --- a/src/lib/stack-health.test.ts +++ b/src/lib/stack-health.test.ts @@ -17,10 +17,9 @@ const mockChecks = { postgresql: { name: "postgresql", status: "up" as const, details: "OK" }, llamaRerank: { name: "llama.cpp-rerank", status: "up" as const, details: "OK" }, coolify: { name: "coolify", status: "up" as const, details: "OK" }, - browserless: { name: "browserless", status: "up" as const, details: "OK" }, langfuse: { name: "langfuse", status: "up" as const, details: "OK" }, qmd: { name: "qmd-mcp", status: "up" as const, details: "OK" }, - llamaGpu: { name: "llama.cpp-gpu", status: "up" as const, details: "OK" }, + llamaGpu: { name: "llama.cpp-gpu", status: "held" as const, details: "port 8001 not reachable — HELD (expected-down)" }, llamaEmbed: { name: "llama.cpp-embed", status: "up" as const, details: "OK" }, searxng: { name: "searxng", status: "up" as const, details: "OK" }, engram: { name: "engram", status: "up" as const, details: "OK" }, @@ -36,6 +35,16 @@ describe("stack-health", () => { expect(summarizeStackHealth(allUp)).toBe("healthy"); }); + it("should return 'healthy' when only up and held checks exist (held does not degrade)", async () => { + const { summarizeStackHealth } = await import("@/lib/stack-health"); + const upAndHeld = Object.values(mockChecks).filter( + c => c.status === "up" || c.status === "held", + ); + expect(upAndHeld.length).toBeGreaterThan(0); + expect(upAndHeld.some(c => c.status === "held")).toBe(true); + expect(summarizeStackHealth(upAndHeld)).toBe("healthy"); + }); + it("should return 'degraded' when any check is down", async () => { const { summarizeStackHealth } = await import("@/lib/stack-health"); const withDown = Object.values(mockChecks); @@ -75,6 +84,33 @@ describe("stack-health", () => { const osintCheck = checks.find(c => c.name === "osint-nexus"); expect(osintCheck).toBeUndefined(); }); + + it("should NOT include ollama (service removed 2026-08-28)", async () => { + const { collectStackServiceChecks } = await import("@/lib/stack-health"); + const checks = await collectStackServiceChecks(); + const ollamaCheck = checks.find(c => c.name === "ollama"); + expect(ollamaCheck).toBeUndefined(); + }); + + it("should NOT include browserless (service uninstalled 2026-09-04)", async () => { + const { collectStackServiceChecks } = await import("@/lib/stack-health"); + const checks = await collectStackServiceChecks(); + const browserlessCheck = checks.find(c => c.name === "browserless"); + expect(browserlessCheck).toBeUndefined(); + }); + + it("should report llama.cpp-gpu as held (intentional stop), never down, while port 8001 is stopped", async () => { + const { collectStackServiceChecks } = await import("@/lib/stack-health"); + const checks = await collectStackServiceChecks(); + const llamaGpu = checks.find(c => c.name === "llama.cpp-gpu"); + expect(llamaGpu).toBeDefined(); + // Port 8001 (Ornith) is HELD — intentionally stopped by the operator. + // If the port listens again the check flips to "up"; it must never be "down". + expect(["held", "up"]).toContain(llamaGpu!.status); + if (llamaGpu!.status === "held") { + expect(llamaGpu!.details).toContain("HELD"); + } + }); }); describe("formatStackHeartbeat", () => { @@ -82,13 +118,16 @@ describe("stack-health", () => { const { formatStackHeartbeat } = await import("@/lib/stack-health"); const checks = [ { name: "test-up", status: "up" as const, details: "OK" }, + { name: "test-held", status: "held" as const, details: "HELD (expected-down)" }, { name: "test-down", status: "down" as const, details: "FAIL" }, ]; const lines = formatStackHeartbeat(checks); expect(lines[0]).toContain("✅"); expect(lines[0]).toContain("test-up"); - expect(lines[1]).toContain("❌"); - expect(lines[1]).toContain("test-down"); + expect(lines[1]).toContain("⏸️"); + expect(lines[1]).toContain("test-held"); + expect(lines[2]).toContain("❌"); + expect(lines[2]).toContain("test-down"); }); }); }); diff --git a/src/lib/stack-health.ts b/src/lib/stack-health.ts index becc02b2..6fce5123 100644 --- a/src/lib/stack-health.ts +++ b/src/lib/stack-health.ts @@ -5,7 +5,12 @@ import { safeExecFile } from "@/lib/safe-exec"; export interface StackServiceCheck { name: string; - status: "up" | "down"; + /** + * "held" = expected-down by explicit operator decision (intentional stop). + * Held services do NOT degrade overall stack health; they are surfaced so + * the dashboard stays truthful without phantom "degraded" states. + */ + status: "up" | "down" | "held"; details: string; } @@ -120,6 +125,34 @@ async function checkTcpService(name: string, port: number): Promise { + const result = await canConnectTcpAny(port, 1000); + + if (result.ok) { + return { + name, + status: "up", + details: `listening on port ${port} (${result.host}) — HELD lifted`, + }; + } + + return { + name, + status: "held", + details: `port ${port} not reachable — HELD (expected-down): ${heldReason}`, + }; +} + async function checkGatewayService(): Promise { const probe = await probeGatewayRuntime(2000); @@ -230,21 +263,30 @@ async function checkHttpService(name: string, url: string, port: number): Promis * Collect health checks for all monitored stack services. * * NOTE: The legacy memory API (port 9077) was removed — migrated to native - * memory-core (SQLite + Ollama nomic-embed). OSINT Nexus (port 8420) was also - * removed from health checks as it is not a core service. + * memory-core (SQLite + nomic-embed via llama.cpp :8002). OSINT Nexus (port + * 8420) was also removed from health checks as it is not a core service. + * Ollama (:11434, service removed 2026-08-28) and browserless (:3002, + * uninstalled 2026-09-04) were dropped as phantom-down sources. + * llama.cpp-gpu (:8001, Ornith) is probed as HELD: intentionally stopped by + * Joker on 2026-09-08 (clean stop, NRestarts=0); production LLM serving runs + * on 1Cat-vLLM Estrella v2 :8009. Revert to a plain TCP check if Ornith + * returns with operator GO. */ export async function collectStackServiceChecks(): Promise { const dockerContainers = parseDockerContainers(); - const [gateway, postgresql, llamaRerank, coolify, browserless, langfuse, qmd, llamaGpu, llamaEmbed, llamaEmbedMem, searxng, engram, prAgent] = await Promise.all([ + const [gateway, postgresql, llamaRerank, coolify, langfuse, qmd, llamaGpu, llamaEmbed, llamaEmbedMem, searxng, engram, prAgent] = await Promise.all([ checkGatewayService(), checkPostgresService(dockerContainers), checkTcpService("llama.cpp-rerank", 8005), checkTcpService("coolify", 8000), - checkHttpService("browserless", "http://127.0.0.1:3002/pressure", 3002), checkHttpService("langfuse", "http://127.0.0.1:3001", 3001), checkTcpService("qmd-mcp", 8181), - checkTcpService("llama.cpp-gpu", 8001), + checkHeldTcpService( + "llama.cpp-gpu", + 8001, + "parada intencional Joker 2026-09-08 (Ornith, card 2ba781a9); serving prod = 1Cat-vLLM :8009; revertir con GO Joker", + ), checkTcpService("llama.cpp-embed", 8002), checkTcpService("llama.cpp-embed-memory", 8006), checkHttpService("searxng", "http://127.0.0.1:8081", 8081), @@ -258,7 +300,6 @@ export async function collectStackServiceChecks(): Promise postgresql, llamaRerank, coolify, - browserless, langfuse, qmd, llamaGpu, @@ -271,11 +312,15 @@ export async function collectStackServiceChecks(): Promise } export function summarizeStackHealth(checks: StackServiceCheck[]): "healthy" | "degraded" { - return checks.every((check) => check.status === "up") ? "healthy" : "degraded"; + return checks.every((check) => check.status !== "down") ? "healthy" : "degraded"; } export function formatStackHeartbeat(checks: StackServiceCheck[]): string[] { - return checks.map( - (check) => `${check.status === "up" ? "✅" : "❌"} ${check.name}: ${check.details}`, - ); + const symbols: Record = { + up: "✅", + held: "⏸️", + down: "❌", + }; + + return checks.map((check) => `${symbols[check.status]} ${check.name}: ${check.details}`); }