#!/usr/bin/env bash # scripts/doctor.sh — S6: Lumina product health checks # # Composes platform-layer health checks: # Docker · nemohermes · openshell · sandbox · policy · skills · inference # # Platform-first: wraps nemohermes / openshell / Docker. No parallel control API. # # Usage: # ./scripts/doctor.sh # full check # ./scripts/doctor.sh --json # machine-readable summary # ./scripts/doctor.sh --help set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Source shared helpers # shellcheck source=lib/common.sh source "$SCRIPT_DIR/lib/common.sh" # shellcheck source=lib/env.sh source "$SCRIPT_DIR/lib/env.sh" # ── Defaults ─────────────────────────────────────────────────────────────── JSON_OUTPUT=0 # ── Parse args ───────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --help|-h) cat </dev/null 2>&1 || true else load_env 2>/dev/null || true fi SANDBOX_NAME="$(get_sandbox_name)" SKILLS_DIR="$REPO_ROOT/skills" POLICY_DIR="$REPO_ROOT/policy/openshell/overlays" # ── Check: Docker ────────────────────────────────────────────────────────── check_docker() { if ! cmd_exists docker; then record_check "Docker" "CLI" "FAIL" "docker command not found" return fi if ! docker info &>/dev/null; then record_check "Docker" "Daemon" "FAIL" "docker daemon not running or not accessible" return fi local version version="$(docker --version 2>/dev/null | sed 's/^Docker version //' | cut -d',' -f1 | tr -d ' ')" record_check "Docker" "Daemon" "OK" "running ($version)" } # ── Check: nemohermes CLI ────────────────────────────────────────────────── check_nemohermes() { if ! cmd_exists nemohermes; then record_check "CLI" "nemohermes" "FAIL" "nemohermes not found — install NemoClaw platform" return fi local version version="$(nemohermes --version 2>/dev/null | head -1 | grep -oP 'v[\d.]+' || echo 'unknown')" record_check "CLI" "nemohermes" "OK" "$version" } # ── Check: openshell CLI ─────────────────────────────────────────────────── check_openshell() { if ! cmd_exists openshell; then record_check "CLI" "openshell" "FAIL" "openshell not found — install OpenShell" return fi local version version="$(openshell --version 2>/dev/null | head -1 | grep -oP '[\d.]+' || echo 'unknown')" record_check "CLI" "openshell" "OK" "$version" } # ── Check: Sandbox status ────────────────────────────────────────────────── check_sandbox() { # Skip if nemohermes is missing (already flagged) if ! cmd_exists nemohermes; then record_check "Sandbox" "Status" "FAIL" "skipped — nemohermes not available" return fi # Run nemohermes doctor for the sandbox — this is the authoritative platform check local doctor_output doctor_output="$(nemohermes "$SANDBOX_NAME" doctor 2>&1)" || true # Check for summary line if echo "$doctor_output" | grep -qi "Summary: healthy"; then record_check "Sandbox" "Doctor" "OK" "$SANDBOX_NAME healthy" elif echo "$doctor_output" | grep -qi "Summary:.*warning"; then record_check "Sandbox" "Doctor" "WARN" "$SANDBOX_NAME has warnings" elif echo "$doctor_output" | grep -qi "Summary:.*unhealthy\|Summary:.*critical"; then record_check "Sandbox" "Doctor" "FAIL" "$SANDBOX_NAME unhealthy" else # Fallback: check status command if nemohermes "$SANDBOX_NAME" status &>/dev/null; then record_check "Sandbox" "Status" "OK" "$SANDBOX_NAME reachable" else record_check "Sandbox" "Status" "FAIL" "$SANDBOX_NAME not found or not reachable" fi fi } # ── Check: Policy overlays ───────────────────────────────────────────────── check_policy() { # Skip if nemohermes is missing if ! cmd_exists nemohermes; then record_check "Policy" "Overlays" "FAIL" "skipped — nemohermes not available" return fi # Check that policy overlay files exist in the repo local inference_policy="$POLICY_DIR/inference.yaml" if [[ ! -f "$inference_policy" ]]; then record_check "Policy" "Overlay files" "WARN" "inference.yaml not found in $POLICY_DIR" fi # Check that lumina-inference preset is applied in the sandbox local policy_list policy_list="$(nemohermes "$SANDBOX_NAME" policy-list 2>&1)" || { record_check "Policy" "policy-list" "FAIL" "could not list policy presets" return } if echo "$policy_list" | grep -q "lumina-inference"; then record_check "Policy" "lumina-inference" "OK" "preset applied" else record_check "Policy" "lumina-inference" "WARN" "preset not found in sandbox — run S5 or: nemohermes $SANDBOX_NAME policy-add --from-file $inference_policy --yes" fi } # ── Check: Skills ────────────────────────────────────────────────────────── check_skills() { if [[ ! -d "$SKILLS_DIR" ]]; then record_check "Skills" "Directory" "FAIL" "skills directory not found at $SKILLS_DIR" return fi # Count skill directories (exclude _lib and hidden) local skill_count=0 local skill_with_md=0 for skill_dir in "$SKILLS_DIR"/*/; do [[ -d "$skill_dir" ]] || continue local name name="$(basename "$skill_dir")" [[ "$name" == "_lib" ]] && continue [[ "$name" == "README.md" ]] && continue skill_count=$((skill_count + 1)) if [[ -f "$skill_dir/SKILL.md" ]]; then skill_with_md=$((skill_with_md + 1)) fi done if [[ $skill_count -eq 0 ]]; then record_check "Skills" "Pack" "WARN" "no skill directories found in $SKILLS_DIR" elif [[ $skill_with_md -lt $skill_count ]]; then record_check "Skills" "Pack" "WARN" "$skill_with_md/$skill_count skills have SKILL.md" else record_check "Skills" "Pack" "OK" "$skill_count skills with SKILL.md" fi } # ── Check: Inference endpoint ────────────────────────────────────────────── check_inference() { # Check .env has the required keys local env_file="${REPO_ROOT}/.env" if [[ ! -f "$env_file" ]]; then record_check "Inference" "Config" "FAIL" ".env not found — run S1 first" return fi # Source .env to get variables (already done by load_env, but verify) local base_url="${LUMINA_INFERENCE_BASE_URL:-}" local model="${LUMINA_INFERENCE_MODEL:-}" if [[ -z "$base_url" ]]; then record_check "Inference" "Endpoint URL" "FAIL" "LUMINA_INFERENCE_BASE_URL not set in .env" return fi if [[ -z "$model" ]]; then record_check "Inference" "Model" "FAIL" "LUMINA_INFERENCE_MODEL not set in .env" return fi # Check endpoint reachability local url="$base_url" # Ensure URL ends with /v1 for the models endpoint if [[ "$url" != */v1 && "$url" != */v1/* ]]; then url="${url%/}/v1" fi if curl -sf --max-time 15 "${url}/models" &>/dev/null; then record_check "Inference" "Endpoint" "OK" "reachable ($base_url)" else record_check "Inference" "Endpoint" "FAIL" "unreachable at $base_url" fi # Check openshell inference config (optional — gateway may not be connected) if cmd_exists openshell; then local inf_output inf_output="$(openshell inference get 2>&1)" || true if echo "$inf_output" | grep -q "Provider:"; then local provider # Strip ANSI escape codes and whitespace provider="$(echo "$inf_output" | grep "Provider:" | head -1 | sed 's/.*Provider: *//' | sed 's/\x1b\[[0-9;]*m//g' | tr -d '[:space:]')" record_check "Inference" "Gateway route" "OK" "configured ($provider)" else record_check "Inference" "Gateway route" "WARN" "not configured via openshell" fi fi } # ── Run all checks ───────────────────────────────────────────────────────── if [[ $JSON_OUTPUT -eq 0 ]]; then log_section "Lumina Doctor (S6)" fi check_docker check_nemohermes check_openshell check_sandbox check_policy check_skills check_inference # ── Output results ───────────────────────────────────────────────────────── if [[ $JSON_OUTPUT -eq 1 ]]; then # Machine-readable JSON summary checks_json="[" first=1 for entry in "${CHECK_RESULTS[@]}"; do IFS='|' read -r group label status detail <<< "$entry" if [[ $first -eq 1 ]]; then first=0 else checks_json+="," fi # Escape backslashes first, then double quotes (JSON-safe) detail="${detail//\\/\\\\}" detail="${detail//\"/\\\"}" checks_json+="{\"group\":\"$group\",\"label\":\"$label\",\"status\":\"$status\",\"detail\":\"$detail\"}" done checks_json+="]" overall="healthy" [[ $CRITICAL_FAIL -eq 1 ]] && overall="unhealthy" cat <