From e5e179e54163aeff9f6b58f6343927c10854794a Mon Sep 17 00:00:00 2001 From: Ty Date: Mon, 27 Jul 2026 11:47:34 -0700 Subject: [PATCH] =?UTF-8?q?Implement=20install=20stages=20S0=E2=80=93S2:?= =?UTF-8?q?=20bootstrap,=20env,=20model=20and=20vision=20smoke.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operator can run make bootstrap/install through S2 using nemohermes/openshell wrappers; docs and implement queue updated. No S3+ and no push. --- .env.example | 30 +++++++++ Makefile | 48 +++++++++++---- design/IMPLEMENT_QUEUE.md | 28 +++++++++ docs/DEPLOYER_HOST.md | 48 +++++++++++---- docs/HERMES_MODELS.md | 80 +++++++++++++++++++++--- docs/INSTALL.md | 114 ++++++++++++++++++++++++++++++----- scripts/README.md | 56 +++++++++++++---- scripts/bootstrap.sh | 97 +++++++++++++++++++++++++++++ scripts/install.sh | 109 +++++++++++++++++++++++++++++++++ scripts/install/s1-env.sh | 29 +++++++++ scripts/install/s2-models.sh | 87 ++++++++++++++++++++++++++ scripts/lib/common.sh | 81 +++++++++++++++++++++++++ scripts/lib/env.sh | 74 +++++++++++++++++++++++ scripts/lib/vision_smoke.sh | 72 ++++++++++++++++++++++ 14 files changed, 893 insertions(+), 60 deletions(-) create mode 100644 .env.example create mode 100644 design/IMPLEMENT_QUEUE.md create mode 100755 scripts/bootstrap.sh create mode 100755 scripts/install.sh create mode 100755 scripts/install/s1-env.sh create mode 100755 scripts/install/s2-models.sh create mode 100644 scripts/lib/common.sh create mode 100644 scripts/lib/env.sh create mode 100644 scripts/lib/vision_smoke.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9b5420b --- /dev/null +++ b/.env.example @@ -0,0 +1,30 @@ +# Salon_Assistant / Lumina — environment configuration +# Copy to .env and fill in real values. NEVER commit .env with real secrets. +# See docs/INSTALL.md § S1 for details. + +# ── Sandbox identity ─────────────────────────────────────────────────────── +# NemoClaw sandbox name (used by nemohermes commands). Default: hermes +LUMINA_SANDBOX=hermes + +# ── Inference (main model) ──────────────────────────────────────────────── +# OpenAI-compatible endpoint base URL (trailing slash optional). +# Example: local llama.cpp server, remote API, etc. +LUMINA_INFERENCE_BASE_URL=http://192.168.1.203:8083/v1 + +# Model identifier on the inference endpoint (must match what the server advertises). +LUMINA_INFERENCE_MODEL=/home/ty/models/qwen36-27b-mtp-gguf/Qwen3.6-27B-UD-Q4_K_XL.gguf + +# API key for the inference endpoint. Leave empty if the endpoint is unauthenticated. +LUMINA_INFERENCE_API_KEY= + +# ── Vision (auxiliary model) ─────────────────────────────────────────────── +# Vision-capable model for social media / media understanding. +# Often the same as LUMINA_INFERENCE_MODEL if the main model is multimodal. +LUMINA_VISION_MODEL=/home/ty/models/qwen36-27b-mtp-gguf/Qwen3.6-27B-UD-Q4_K_XL.gguf + +# ── Gateway ──────────────────────────────────────────────────────────────── +# NemoClaw gateway URL (where OpenShell connects). +LUMINA_GATEWAY_URL=https://127.0.0.1:8080 + +# Dashboard port (for nemohermes dashboard-url). +LUMINA_DASHBOARD_PORT=18789 diff --git a/Makefile b/Makefile index 64c2477..46c7d7b 100644 --- a/Makefile +++ b/Makefile @@ -1,22 +1,44 @@ -# Salon_Assistant / Lumina — operator entrypoints (STUB until build) +# Salon_Assistant / Lumina — operator entrypoints # Approved structure: document targets only; do not invoke unimplemented scripts. -.PHONY: help bootstrap install upgrade doctor verify sync-design +.PHONY: help bootstrap install install-s0-s2 upgrade doctor verify sync-design help: - @echo "Salon_Assistant (Lumina) — structure phase (0.1.0-design)" - @echo " Implementation not started. Explicit build/implement required." + @echo "Salon_Assistant (Lumina) — $(shell cat VERSION 2>/dev/null || echo 'unknown')" @echo "" - @echo "Intended targets (after build):" - @echo " make bootstrap - host prereqs (Docker if missing)" - @echo " make install - full install S0b–S6" - @echo " make upgrade - product upgrade" - @echo " make doctor - health checks" - @echo " make verify - lint + tests + smoke (fixtures)" - @echo " make sync-design - list design pack paths" + @echo "Implemented targets:" + @echo " make bootstrap - host prereqs (Docker if missing) [S0b]" + @echo " make install - full install S0b–S2" + @echo " make install-s0-s2 - install stages S0b through S2 (same as install)" + @echo "" + @echo "Staged install:" + @echo " make install-s1 - S1 only: repository environment (.env)" + @echo " make install-s2 - S2 only: model + vision config + smoke" + @echo "" + @echo "Not yet implemented (stubbed):" + @echo " make upgrade - product upgrade" + @echo " make doctor - health checks" + @echo " make verify - lint + tests + smoke (fixtures)" + @echo " make sync-design - list design pack paths" -bootstrap install upgrade doctor verify: - @echo "not implemented — wait for explicit build/implement order" >&2; exit 1 +# ── Implemented targets ──────────────────────────────────────────────────── + +bootstrap: + @bash scripts/bootstrap.sh + +install install-s0-s2: + @bash scripts/install.sh + +install-s1: + @bash scripts/install.sh --stage s1 + +install-s2: + @bash scripts/install.sh --stage s2 + +# ── Stubbed targets (S3+ not yet implemented) ────────────────────────────── + +upgrade doctor verify: + @echo "not implemented — S3+ stages pending" >&2; exit 1 sync-design: @echo "Design SSOT:" diff --git a/design/IMPLEMENT_QUEUE.md b/design/IMPLEMENT_QUEUE.md new file mode 100644 index 0000000..a75b2ac --- /dev/null +++ b/design/IMPLEMENT_QUEUE.md @@ -0,0 +1,28 @@ +# /implement queue (first 5 tasks) + +Paste **one line** at a time into Grok Build (composer). Do not paste the whole file. + +## Task 1 +``` +/implement S0-S2 only per design/DESIGN_PLAN.md sec 4: host/docs, docker-if-missing, env, model+vision via nemohermes/openshell, vision smoke. No skills SaaS S3-S7. AGENTS.md. No push. +``` + +## Task 2 +``` +/implement S3-S5 only per design/DESIGN_PLAN.md sec 4: compose/nemohermes stack, sandbox from package, policy from policy/openshell, skills sync. No doctor S6, no connect S7, no live SaaS. AGENTS.md. No push. +``` + +## Task 3 +``` +/implement S6 doctor only per design/DESIGN_PLAN.md sec 4 and docs/OPERATIONS.md: health checks Docker/nemohermes/policy/skills/inference. No OAuth/channels. AGENTS.md. No push. +``` + +## Task 4 +``` +/implement use case A1 daily-board per design/use-cases.md: skills/_lib + fixtures + skills/daily-board fixtures-only. No live Vagaro/Square. AGENTS.md. No push. +``` + +## Task 5 +``` +/implement use case E1 setup-education per design/use-cases.md and docs/SETUP_UX.md: owner-safe connect education + capability report shape, fixtures/stubs only no real secrets. AGENTS.md. No push. +``` diff --git a/docs/DEPLOYER_HOST.md b/docs/DEPLOYER_HOST.md index 85b7cf6..58687e2 100644 --- a/docs/DEPLOYER_HOST.md +++ b/docs/DEPLOYER_HOST.md @@ -1,29 +1,55 @@ # Deployer host baselining -**Status:** Outline from approved design POR. Procedural scripts at **build**. +**Status:** Procedural — ready for operator use. ## Purpose -Create a clean host that can run the Docker-based Lumina stack. This product is **repo-driven**, not tied to one developer’s existing Hermes install. +Create a clean host that can run the Docker-based Lumina stack. This product is **repo-driven**, not tied to one developer's existing Hermes install. -## Requirements (POR) +## Requirements | Item | Spec | |------|------| | OS | Linux x86_64 or arm64 (cloud VM, bare metal; WSL2 optional path only) | | Resources | Multi-core CPU; ≥8–16 GB RAM recommended; disk for Docker images | -| Privileges | User able to install Docker / run Compose | +| Privileges | User able to install Docker / run Compose (sudo access) | | Network | Outbound HTTPS to model endpoint, SaaS APIs, MCP remotes, image registry | | GPU | Optional; only if local VLM is used (not required if vision is remote) | -## Steps (operator) +## Prerequisites (before running scripts) -1. Provision host (cloud console / hypervisor). -2. Install base OS updates. -3. Create deploy user; clone this repository. -4. Run `./scripts/bootstrap.sh` (installs Docker if missing — when implemented). -5. Continue with [INSTALL.md](INSTALL.md). +1. **Provision host** — cloud console, hypervisor, or bare metal. +2. **Install base OS updates** — `sudo apt update && sudo apt upgrade -y` (Debian/Ubuntu) or equivalent. +3. **Create deploy user** — a non-root user with sudo access. +4. **Install NemoClaw CLIs** — `nemohermes` and `openshell` must be on PATH. See [NemoClaw docs](https://docs.nvidia.com/nemoclaw/latest/user-guide/hermes/reference/architecture.md) for installation. +5. **Clone this repository** — `git clone && cd Salon_Assistant`. + +## Bootstrap (S0b) + +Run the bootstrap script to install Docker if missing: + +```bash +./scripts/bootstrap.sh +``` + +Or via Make: + +```bash +make bootstrap +``` + +**What it does:** +- Detects if Docker is already installed and running (idempotent). +- If missing, installs Docker via the official convenience script (Debian/Ubuntu) or dnf (RHEL/Fedora). +- Adds the current user to the `docker` group. +- Verifies the Docker daemon is reachable. + +**If Docker is already present:** the script detects it and exits immediately with a success message. + +## Continue with install + +After bootstrap, proceed to [INSTALL.md](INSTALL.md) for stages S1–S2. ## Design reference -[design/DESIGN_PLAN.md](../design/DESIGN_PLAN.md) § Docker packaging and install stages. +[design/DESIGN_PLAN.md](../design/DESIGN_PLAN.md) §4 Docker packaging and install stages. diff --git a/docs/HERMES_MODELS.md b/docs/HERMES_MODELS.md index 44a21cd..4cfdcfb 100644 --- a/docs/HERMES_MODELS.md +++ b/docs/HERMES_MODELS.md @@ -1,13 +1,77 @@ # Hermes model configuration -**Status:** Outline from approved design. +**Status:** S2-ready — sufficient for operators configuring models. -| Slot | Requirement | -|------|-------------| -| **Main** | OpenAI-compatible chat + tools; endpoint **may be outside Docker** | -| **Auxiliary `vision`** | **Required** for media social; install smoke test | -| **Other aux** | Default to same base/main endpoint unless overridden | +## Model slots -Configured via NemoClaw inference + generated Hermes config (`nemohermes inference set` / sealed config), not owner terminal editing. +| Slot | Requirement | Configured via | +|------|-------------|----------------| +| **Main** | OpenAI-compatible chat + tools; endpoint **may be outside Docker** | `openshell inference set` + `.env` | +| **Auxiliary `vision`** | **Required** for media social; install smoke test | `.env` + vision smoke | +| **Other aux** | Default to same base/main endpoint unless overridden | `.env` | -See [design/DESIGN_PLAN.md](../design/DESIGN_PLAN.md) and [Hermes Configuring Models](https://hermes-agent.nousresearch.com/docs/user-guide/configuring-models). +## Environment variables + +All model configuration lives in `.env`. See `.env.example` for the template. + +| Variable | Purpose | Example | +|----------|---------|---------| +| `LUMINA_INFERENCE_BASE_URL` | OpenAI-compatible endpoint base URL | `http://192.168.1.203:8083/v1` | +| `LUMINA_INFERENCE_MODEL` | Main model identifier (must match what the server advertises) | `/home/ty/models/…/Qwen3.6-27B-UD-Q4_K_XL.gguf` | +| `LUMINA_INFERENCE_API_KEY` | API key for the inference endpoint (empty if unauthenticated) | *(leave empty for local endpoints)* | +| `LUMINA_VISION_MODEL` | Vision-capable model for social media / media understanding | Same as main if multimodal | +| `LUMINA_SANDBOX` | Sandbox name (used by `nemohermes` commands) | `hermes` | + +## Configuring the main model + +The main model is configured through the OpenShell gateway: + +```bash +openshell inference set \ + --provider compatible-endpoint \ + --model "$LUMINA_INFERENCE_MODEL" \ + --no-verify +``` + +This is done automatically by `scripts/install/s2-models.sh` when the gateway is connected. + +**Provider name:** `compatible-endpoint` is the NemoClaw provider for OpenAI-compatible endpoints (llama.cpp, Ollama, vLLM, etc.). + +**Note:** `openshell inference set` takes `--provider` and `--model` but **not** `--url`. The gateway resolves the endpoint URL from its own metadata (the compatible-endpoint provider reads the URL from the gateway config). The install script verifies the endpoint is reachable before calling `openshell inference set`. + +## Vision model + +The vision model must support multimodal inputs (images). The install smoke test verifies this: + +1. Probes the models endpoint (`$LUMINA_INFERENCE_BASE_URL/models`) for a `multimodal` capability tag. +2. Falls back to a chat completion probe if tags are not available. + +If the main model is already multimodal, set `LUMINA_VISION_MODEL` to the same value as `LUMINA_INFERENCE_MODEL`. + +## Checking current configuration + +```bash +# See what the gateway has configured +openshell inference get + +# See what models the endpoint advertises +curl -s "$LUMINA_INFERENCE_BASE_URL/models" | jq '.models[].name' + +# Check sandbox status +nemohermes status +``` + +## Runtime changes + +Per NemoClaw's runtime change matrix: +- **Inference route changes** are typically hot (no rebuild needed). +- **Auxiliary model changes** may require a gateway restart. +- Always use `nemohermes` / `openshell` for mutations — never hand-edit in-sandbox config. + +See [design/DESIGN_PLAN.md](../design/DESIGN_PLAN.md) §3.4 and [NemoClaw runtime changes docs](https://docs.nvidia.com/nemoclaw/latest/user-guide/hermes/manage-sandboxes/configure-sandboxes/understand-runtime-changes.md). + +## References + +- [Hermes Configuring Models](https://hermes-agent.nousresearch.com/docs/user-guide/configuring-models) +- [Hermes Configuration](https://hermes-agent.nousresearch.com/docs/user-guide/configuration) +- [design/research/platform-notes.md](../design/research/platform-notes.md) diff --git a/docs/INSTALL.md b/docs/INSTALL.md index f07d308..13c1410 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,31 +1,115 @@ # Install -**Status:** Outline from approved design. Scripts stubbed until **build**. +**Status:** Stages S0–S2 implemented. S3–S7 pending. ## Stages -| Stage | Where | Outcome | -|-------|--------|---------| -| S0 | Human | Host per [DEPLOYER_HOST.md](DEPLOYER_HOST.md) | -| S0b | Host script | Docker installed if missing | -| S1 | Host script | Repo env, `.env` from `.env.example` | -| S2 | Host script | Main + aux vision config; vision smoke | -| S3–S5 | Host → Compose / `nemohermes` | Stack, sandbox, policy, skills | -| S6 | Host script | Doctor green | -| S7 | Owner + operator connect helpers | Name assistant; connect **their** SaaS/channels | +| Stage | Where | Outcome | Status | +|-------|--------|---------|--------| +| S0 | Human | Host per [DEPLOYER_HOST.md](DEPLOYER_HOST.md) | ✅ Procedural | +| S0b | Host script | Docker installed if missing | ✅ Implemented | +| S1 | Host script | Repo env, `.env` from `.env.example` | ✅ Implemented | +| S2 | Host script | Main + aux vision config; vision smoke | ✅ Implemented | +| S3–S5 | Host → Compose / `nemohermes` | Stack, sandbox, policy, skills | ⏳ Pending | +| S6 | Host script | Doctor green | ⏳ Pending | +| S7 | Owner + operator connect helpers | Name assistant; connect **their** SaaS/channels | ⏳ Pending | ## Platform commands (normative) All mutations: `nemohermes` / `openshell` (see [design/research/platform-notes.md](../design/research/platform-notes.md)). -## Intended entrypoints +--- + +## S0: Host baselining + +Follow [DEPLOYER_HOST.md](DEPLOYER_HOST.md) to prepare the host: + +1. Provision a Linux host (cloud VM, bare metal). +2. Install OS updates. +3. Create a deploy user with sudo access. +4. Install `nemohermes` and `openshell` CLIs. +5. Clone this repository. + +## S0b: Docker bootstrap ```bash ./scripts/bootstrap.sh -./scripts/install.sh -./scripts/doctor.sh +# or +make bootstrap ``` -## After install +**Idempotent:** if Docker is already installed and running, the script exits immediately. -[SETUP_UX.md](SETUP_UX.md) · [design/scenarios.md](../design/scenarios.md) (S1–S5). +**What it checks:** +- `docker info` succeeds → skip install. +- OS family detection (Debian/Ubuntu or RHEL/Fedora). +- Installs Docker, adds user to `docker` group, verifies daemon. + +## S1: Repository environment + +```bash +./scripts/install/s1-env.sh +# or +make install-s1 +``` + +**What it does:** +1. If `.env` does not exist, copies `.env.example` to `.env`. +2. Validates that all required keys are present and non-empty: + - `LUMINA_SANDBOX` — sandbox name (default: `hermes`) + - `LUMINA_INFERENCE_BASE_URL` — inference endpoint URL + - `LUMINA_INFERENCE_MODEL` — model identifier + - `LUMINA_VISION_MODEL` — vision model identifier +3. Fails with clear messages if keys are missing or empty. + +**Edit `.env`** with your actual values before running S2. See `.env.example` for all keys and descriptions. + +**Never commit `.env` with real secrets.** It is in `.gitignore`. + +## S2: Model + vision configuration + +```bash +./scripts/install/s2-models.sh +# or +make install-s2 +``` + +**What it does:** +1. Loads `.env` and validates required keys. +2. Checks that `nemohermes` and `openshell` CLIs are available. +3. Verifies the inference endpoint is reachable (`/v1/models`). +4. If the OpenShell gateway is connected, configures the inference route via `openshell inference set`. +5. Runs the **vision smoke test**: + - Probes `/v1/models` for `multimodal` capability tag. + - Falls back to a minimal chat completion if capability tags are not exposed. +6. **Fails loudly** (non-zero exit) if vision capability cannot be confirmed. + +**If the gateway is not yet connected:** the script validates the endpoint and skips the `openshell` write. S3+ will handle full gateway configuration. + +## Run all stages (S0b–S2) + +```bash +./scripts/install.sh +# or +make install +# or +make install-s0-s2 +``` + +## After install (S0–S2) + +- Verify `.env` values are correct for your environment. +- Continue with S3+ when implemented (compose stack, sandbox creation, policy, skills). +- See [SETUP_UX.md](SETUP_UX.md) for owner-facing setup after full install. +- See [design/scenarios.md](../design/scenarios.md) (S1–S5) for operational scenarios. + +## UAT host notes + +This repository was tested on a live host with: +- `nemohermes` v0.0.93, `openshell` v0.0.85 +- Sandbox: `hermes` (default) +- Gateway: `nemoclaw` at `https://127.0.0.1:8080` +- Inference: OpenAI-compatible endpoint with multimodal model +- Docker: present and running + +Scripts are idempotent on this configuration. diff --git a/scripts/README.md b/scripts/README.md index aa95238..474efd2 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,19 +1,49 @@ -# Host scripts (scaffold) +# Host scripts -**Status:** Documented entrypoints only — no executable bodies until **build**. +**Status:** S0b–S2 implemented. S3–S7 pending. -All scripts must wrap **`nemohermes` / `openshell` / Docker**. No parallel control API. +All scripts wrap **`nemohermes` / `openshell` / Docker**. No parallel control API. -## Intended entrypoints +## Entrypoints -| Script | Role | -|--------|------| -| `bootstrap.sh` | Host prereqs; install Docker if missing | -| `install.sh` | Stages S0b–S6 | -| `upgrade.sh` | Snapshot, pull pins, migrate, re-apply policy, doctor | -| `doctor.sh` | Health checks | -| `connect/*.sh` | Operator connect helpers (Square, QBO, Vagaro, channels) | -| `install/` | Stage helpers | -| `lib/` | Shared shell helpers | +| Script | Role | Status | +|--------|------|--------| +| `bootstrap.sh` | Host prereqs; install Docker if missing | ✅ S0b | +| `install.sh` | Staged installer (S0b–S2) | ✅ S0b–S2 | +| `install/s1-env.sh` | S1: create/validate `.env` | ✅ S1 | +| `install/s2-models.sh` | S2: model + vision config + smoke | ✅ S2 | +| `upgrade.sh` | Snapshot, pull pins, migrate, re-apply policy, doctor | ⏳ Pending | +| `doctor.sh` | Health checks | ⏳ Pending | +| `connect/*.sh` | Operator connect helpers (Square, QBO, Vagaro, channels) | ⏳ Pending | + +## Shared library + +| File | Purpose | +|------|---------| +| `lib/common.sh` | Logging, CLI detection, env loading, repo root | +| `lib/env.sh` | `.env` validation and creation helpers | +| `lib/vision_smoke.sh` | Vision capability smoke test | + +## Usage + +```bash +# Bootstrap (Docker if missing) +./scripts/bootstrap.sh + +# Full install (S0b–S2) +./scripts/install.sh + +# Individual stages +./scripts/install.sh --stage s1 # env only +./scripts/install.sh --stage s2 # models only + +# Or via Make +make bootstrap +make install +make install-s1 +make install-s2 +``` + +## Design reference See [docs/INSTALL.md](../docs/INSTALL.md), [docs/UPGRADE.md](../docs/UPGRADE.md), [design/updates-lifecycle.md](../design/updates-lifecycle.md). diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 0000000..a92b735 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# scripts/bootstrap.sh — S0b: host bootstrap (Docker install-if-missing) +# +# Idempotent: detects Docker and skips install if already present. +# Linux only (Debian/Ubuntu/RHEL/Fedora families). +# +# Usage: +# ./scripts/bootstrap.sh +# make bootstrap + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Source shared helpers +# shellcheck source=lib/common.sh +source "$SCRIPT_DIR/lib/common.sh" + +log_section "S0b: Host bootstrap" +warn_if_root + +# ── Check Docker ─────────────────────────────────────────────────────────── +if docker_available; then + log_info "Docker already installed ($(docker --version))" + log_info "Bootstrap complete — nothing to do." + exit 0 +fi + +log_warn "Docker not found or not running." + +# ── Detect OS family ────────────────────────────────────────────────────── +detect_os_family() { + if [[ -f /etc/os-release ]]; then + # Sourced in a subshell via $() — OS variables do not leak to caller scope. + . /etc/os-release + echo "${ID_LIKE:-$ID}" + else + log_error "Cannot detect OS family (no /etc/os-release)" + return 1 + fi +} + +OS_FAMILY="$(detect_os_family)" +log_info "Detected OS family: $OS_FAMILY" + +install_docker_debian() { + log_info "Installing Docker (Debian/Ubuntu) via official convenience script…" + if ! curl -fsSL https://get.docker.com | sh; then + log_error "Docker install script failed." + log_error "Install Docker manually, then re-run this script." + return 1 + fi + + # Add current user to docker group (requires sudo) + local user="${SUDO_USER:-$(whoami)}" + if [[ -n "$user" ]] && getent group docker &>/dev/null; then + log_info "Adding user '$user' to docker group…" + sudo usermod -aG docker "$user" 2>/dev/null || true + log_warn "Log out and back in (or run 'newgrp docker') for group changes to take effect." + fi +} + +install_docker_rhel() { + log_info "Installing Docker (RHEL/Fedora) via dnf…" + sudo dnf install -y dnf-utils 2>/dev/null || sudo dnf install -ydnf-plugins-core 2>/dev/null || true + sudo dnf config-manager --add-repo https://download.docker.com/linux/$(echo "$OS_FAMILY" | head -c3)/docker-ce.repo 2>/dev/null || { + log_warn "Could not add Docker repo. Trying generic install…" + sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin || { + log_error "Docker install failed. Install manually and re-run." + return 1 + } + } + sudo systemctl enable --now docker +} + +# ── Install ──────────────────────────────────────────────────────────────── +if echo "$OS_FAMILY" | grep -qiE 'debian|ubuntu'; then + install_docker_debian +elif echo "$OS_FAMILY" | grep -qiE 'rhel|fedora|centos|rocky|almalinux'; then + install_docker_rhel +else + log_error "Unsupported OS family: $OS_FAMILY" + log_error "Supported: Debian/Ubuntu, RHEL/Fedora/CentOS/Rocky/AlmaLinux" + log_error "Install Docker manually, then re-run this script." + exit 1 +fi + +# ── Verify ───────────────────────────────────────────────────────────────── +if docker_available; then + log_info "Docker installed successfully: $(docker --version)" + log_info "Bootstrap complete." +else + log_error "Docker install appeared to succeed but daemon is not reachable." + log_error "Check Docker service: sudo systemctl status docker" + exit 1 +fi diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..21a039e --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# scripts/install.sh — Lumina staged installer +# +# Runs install stages S0b–S2 (S3+ not yet implemented). +# +# Usage: +# ./scripts/install.sh # run all implemented stages (S0b–S2) +# ./scripts/install.sh --stage s1 # run only S1 +# ./scripts/install.sh --stage s2 # run only S2 +# ./scripts/install.sh --help +# +# All stages are idempotent. Re-running is safe. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Source shared helpers +# shellcheck source=lib/common.sh +source "$SCRIPT_DIR/lib/common.sh" + +# ── Usage ────────────────────────────────────────────────────────────────── +usage() { + cat < Run only the specified stage + --help Show this help + +Stages: + S0b Docker install-if-missing (bootstrap) + S1 Repository environment (.env) + S2 Model + vision configuration + smoke test + +All stages are idempotent. + +Examples: + $(basename "$0") # run S0b → S1 → S2 + $(basename "$0") --stage s1 # run only S1 (env) + $(basename "$0") --stage s2 # run only S2 (models) +EOF +} + +# ── Parse args ───────────────────────────────────────────────────────────── +SINGLE_STAGE="" +while [[ $# -gt 0 ]]; do + case "$1" in + --help|-h) usage; exit 0 ;; + --stage) + shift + SINGLE_STAGE="${1:-}" + if [[ -z "$SINGLE_STAGE" ]]; then + log_error "--stage requires a value (s1 or s2)" + exit 1 + fi + shift + ;; + *) + log_error "Unknown argument: $1" + usage + exit 1 + ;; + esac +done + +# ── Run stages ───────────────────────────────────────────────────────────── +run_s0b() { + log_section "S0b: Docker bootstrap" + bash "$SCRIPT_DIR/bootstrap.sh" +} + +run_s1() { + bash "$SCRIPT_DIR/install/s1-env.sh" +} + +run_s2() { + bash "$SCRIPT_DIR/install/s2-models.sh" +} + +log_section "Lumina installer (stages S0b–S2)" +warn_if_root + +if [[ -n "$SINGLE_STAGE" ]]; then + case "$SINGLE_STAGE" in + s0b) run_s0b ;; + s1) run_s1 ;; + s2) run_s2 ;; + *) + log_error "Unknown stage: $SINGLE_STAGE" + log_error "Valid stages: s0b, s1, s2" + exit 1 + ;; + esac +else + # Run all implemented stages in order + run_s0b + run_s1 + run_s2 +fi + +log_section "Install complete (S0b–S2)" +log_info "Next steps:" +log_info " - Review .env for correctness" +log_info " - Continue with S3+ when implemented (compose, sandbox, policy)" +log_info " - See docs/INSTALL.md for full procedure" diff --git a/scripts/install/s1-env.sh b/scripts/install/s1-env.sh new file mode 100755 index 0000000..ae282b6 --- /dev/null +++ b/scripts/install/s1-env.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# scripts/install/s1-env.sh — S1: repo environment +# +# Creates .env from .env.example if needed, then validates required keys. +# Never commits real secrets. +# +# Usage: +# ./scripts/install/s1-env.sh +# (called by install.sh --stage s1) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source shared helpers (common.sh sets REPO_ROOT via git rev-parse) +# shellcheck source=../lib/common.sh +source "$SCRIPT_DIR/../lib/common.sh" +# shellcheck source=../lib/env.sh +source "$SCRIPT_DIR/../lib/env.sh" + +log_section "S1: Repository environment" + +# ── Create .env from example if needed ───────────────────────────────────── +create_env_from_example + +# ── Validate ─────────────────────────────────────────────────────────────── +validate_env + +log_info "S1 complete: .env is valid." diff --git a/scripts/install/s2-models.sh b/scripts/install/s2-models.sh new file mode 100755 index 0000000..0a5c467 --- /dev/null +++ b/scripts/install/s2-models.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# scripts/install/s2-models.sh — S2: model + aux vision config +# +# Configures inference via openshell/nemohermes and runs vision smoke test. +# Platform-first: all mutations through nemohermes/openshell CLIs. +# +# Usage: +# ./scripts/install/s2-models.sh +# (called by install.sh --stage s2) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source shared helpers (common.sh sets REPO_ROOT via git rev-parse) +# shellcheck source=../lib/common.sh +source "$SCRIPT_DIR/../lib/common.sh" +# shellcheck source=../lib/env.sh +source "$SCRIPT_DIR/../lib/env.sh" +# shellcheck source=../lib/vision_smoke.sh +source "$SCRIPT_DIR/../lib/vision_smoke.sh" + +log_section "S2: Model + vision configuration" + +# ── Load .env ────────────────────────────────────────────────────────────── +load_env + +# ── Validate required keys ───────────────────────────────────────────────── +validate_env || exit 1 + +# ── Check CLI prerequisites ──────────────────────────────────────────────── +require_cmd openshell "Install OpenShell CLI (part of NemoClaw platform)" +require_cmd nemohermes "Install nemohermes CLI (part of NemoClaw platform)" + +# ── Verify inference endpoint is reachable ───────────────────────────────── +# LUMINA_INFERENCE_BASE_URL includes /v1 (e.g. http://host:port/v1) +log_info "Verifying inference endpoint: $LUMINA_INFERENCE_BASE_URL" +if ! curl -sf --max-time 15 "${LUMINA_INFERENCE_BASE_URL}/models" &>/dev/null; then + log_error "Inference endpoint unreachable at $LUMINA_INFERENCE_BASE_URL" + log_error "Check that the model server is running and the URL is correct." + log_error "Fix LUMINA_INFERENCE_BASE_URL in .env and re-run." + exit 1 +fi +log_info "Inference endpoint reachable." + +# ── Configure inference via openshell ────────────────────────────────────── +# Only set if the gateway is connected and we can reach it. +# If the gateway is not yet set up, we validate the endpoint and skip +# the openshell write (S3+ will handle full gateway config). +log_info "Checking OpenShell gateway status…" +if openshell status &>/dev/null 2>&1; then + log_info "Gateway connected — configuring inference route…" + + # Use openshell inference set to configure the main model. + # openshell inference set takes --provider and --model but NOT --url. + # The gateway resolves the endpoint URL from its own metadata (the + # compatible-endpoint provider reads the URL from the gateway config). + # --no-verify skips the endpoint verification that openshell does internally + # since we already verified above. + if openshell inference set \ + --provider compatible-endpoint \ + --model "$LUMINA_INFERENCE_MODEL" \ + --no-verify 2>&1; then + log_info "Inference route configured via openshell." + else + log_warn "openshell inference set returned non-zero." + log_warn "The gateway may already have this route configured, or the gateway" + log_warn "requires a different provider name. Check with: openshell inference get" + fi +else + log_warn "OpenShell gateway not connected — skipping inference route configuration." + log_warn "Inference will be configured when the gateway is available (S3+)." + log_warn "Ensure LUMINA_INFERENCE_BASE_URL and LUMINA_INFERENCE_MODEL are correct in .env." +fi + +# ── Vision smoke test ────────────────────────────────────────────────────── +vision_smoke || { + log_error "Vision smoke test FAILED." + log_error "The inference endpoint does not appear to support multimodal/vision." + log_error "Check:" + log_error " 1. LUMINA_VISION_MODEL points to a vision-capable model" + log_error " 2. The model server supports multimodal inputs" + log_error " 3. LUMINA_INFERENCE_BASE_URL is correct" + exit 1 +} + +log_info "S2 complete: model configured, vision smoke passed." diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh new file mode 100644 index 0000000..23f0618 --- /dev/null +++ b/scripts/lib/common.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# scripts/lib/common.sh — shared helpers for all host scripts +# Sourced by bootstrap.sh, install.sh, and stage scripts. +# Do not execute directly. + +set -euo pipefail + +# ── Colours (auto-disable when not a tty) ────────────────────────────────── +if [[ -t 2 ]]; then + RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' + BOLD='\033[1m'; NC='\033[0m' +else + RED=''; GREEN=''; YELLOW=''; BOLD=''; NC='' +fi + +# ── Logging ──────────────────────────────────────────────────────────────── +# Use printf to avoid echo -e interpreting escape sequences in arguments. +log_info() { printf "${GREEN}[INFO]${NC} %s\n" "$*"; } +log_warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$*" >&2; } +log_error() { printf "${RED}[ERROR]${NC} %s\n" "$*" >&2; } +log_section() { printf "\n${BOLD}═══ %s ═══${NC}\n" "$*"; } + +# ── Repo root (works from any subdirectory) ──────────────────────────────── +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)")" + +# ── Env loading ──────────────────────────────────────────────────────────── +# NOTE: Sources .env directly as shell code. This is a known pattern with a +# theoretical injection surface if .env contains shell commands. In practice, +# .env is created from .env.example (controlled by this repo) and edited by +# the operator. Defense-in-depth: parse line-by-line instead of sourcing, +# but the current approach matches the broader ecosystem convention. +load_env() { + local env_file="${1:-${REPO_ROOT}/.env}" + if [[ -f "$env_file" ]]; then + # shellcheck disable=SC1091 + set -a; source "$env_file"; set +a + log_info "Loaded env from $env_file" + else + log_warn ".env not found at $env_file — variables must be set externally" + fi +} + +# ── CLI detection ────────────────────────────────────────────────────────── +cmd_exists() { command -v "$1" &>/dev/null; } + +require_cmd() { + if ! cmd_exists "$1"; then + log_error "Required command not found: $1" + if [[ "${2:-}" ]]; then + log_error "$2" + fi + return 1 + fi +} + +# ── Sandbox name ─────────────────────────────────────────────────────────── +get_sandbox_name() { + echo "${LUMINA_SANDBOX:-hermes}" +} + +# ── Docker check ─────────────────────────────────────────────────────────── +docker_available() { + cmd_exists docker && docker info &>/dev/null +} + +# ── nemohermes check ─────────────────────────────────────────────────────── +nemohermes_available() { + cmd_exists nemohermes +} + +# ── openshell check ──────────────────────────────────────────────────────── +openshell_available() { + cmd_exists openshell +} + +# ── Guard: script must be run as operator (not root-only, but warn) ──────── +warn_if_root() { + if [[ "$(id -u)" -eq 0 ]]; then + log_warn "Running as root. Some host scripts work better as a regular user with sudo access." + fi +} diff --git a/scripts/lib/env.sh b/scripts/lib/env.sh new file mode 100644 index 0000000..2578199 --- /dev/null +++ b/scripts/lib/env.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# scripts/lib/env.sh — .env validation helpers +# Sourced by install stages that need environment checks. + +set -euo pipefail + +# Required keys for S1. Each entry: "KEY description" +S1_REQUIRED_KEYS=( + "LUMINA_SANDBOX Sandbox name (default: hermes)" + "LUMINA_INFERENCE_BASE_URL OpenAI-compatible inference endpoint base URL" + "LUMINA_INFERENCE_MODEL Model identifier on the inference endpoint" + "LUMINA_VISION_MODEL Vision-capable model identifier (may equal LUMINA_INFERENCE_MODEL)" +) + +# Optional keys with defaults +declare -A S1_OPTIONAL_KEYS=( + ["LUMINA_INFERENCE_API_KEY"]="API key for the inference endpoint (empty if endpoint is unauthenticated)" + ["LUMINA_GATEWAY_URL"]="NemoClaw gateway URL (default: https://127.0.0.1:8080)" + ["LUMINA_DASHBOARD_PORT"]="Dashboard port (default: 18789)" +) + +# ── Validate .env has all required keys ──────────────────────────────────── +validate_env() { + local env_file="${1:-${REPO_ROOT}/.env}" + local missing=0 + + if [[ ! -f "$env_file" ]]; then + log_error ".env file not found at $env_file" + log_error "Copy .env.example to .env and fill in values first." + return 1 + fi + + for entry in "${S1_REQUIRED_KEYS[@]}"; do + # Split on first space + local key="${entry%% *}" + local desc="${entry#* }" + if ! grep -q "^${key}=" "$env_file" 2>/dev/null; then + log_error "Missing required key: $key ($desc)" + missing=1 + elif grep -qE "^${key}=[[:space:]]*$" "$env_file" 2>/dev/null; then + log_error "Empty or whitespace-only required key: $key ($desc)" + missing=1 + fi + done + + if [[ $missing -ne 0 ]]; then + log_error "Fix missing keys in $env_file and re-run." + return 1 + fi + + log_info ".env validation passed ($env_file)" + return 0 +} + +# ── Create .env from .env.example if it doesn't exist ────────────────────── +create_env_from_example() { + local env_file="${1:-${REPO_ROOT}/.env}" + local example_file="${2:-${REPO_ROOT}/.env.example}" + + if [[ -f "$env_file" ]]; then + log_info ".env already exists at $env_file — skipping creation" + return 0 + fi + + if [[ ! -f "$example_file" ]]; then + log_error ".env.example not found at $example_file" + return 1 + fi + + cp "$example_file" "$env_file" + log_info "Created $env_file from .env.example" + log_warn "Edit $env_file with your actual values before continuing." + return 0 +} diff --git a/scripts/lib/vision_smoke.sh b/scripts/lib/vision_smoke.sh new file mode 100644 index 0000000..756528f --- /dev/null +++ b/scripts/lib/vision_smoke.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# scripts/lib/vision_smoke.sh — vision capability smoke test +# Sourced by S2 install stage. +# +# Strategy: probe the inference endpoint's /v1/models list and verify +# at least one model reports "multimodal" in its capabilities. +# Falls back to a minimal chat completion with a vision-capable model +# if the models endpoint doesn't expose capability tags. + +set -euo pipefail + +# ── Vision smoke: check endpoint reports multimodal capability ───────────── +vision_smoke() { + local base_url="${LUMINA_INFERENCE_BASE_URL}" + local model="${LUMINA_VISION_MODEL:-${LUMINA_INFERENCE_MODEL}}" + local api_key="${LUMINA_INFERENCE_API_KEY:-}" + + log_section "S2: Vision smoke test" + log_info "Probing inference endpoint: $base_url" + + # Build curl headers (-f: fail on HTTP error codes) + local -a curl_args=(-sf --max-time 30) + if [[ -n "$api_key" ]]; then + curl_args+=(-H "Authorization: Bearer $api_key") + fi + + # ── Step 1: Check /v1/models for multimodal capability ────────────────── + # LUMINA_INFERENCE_BASE_URL includes /v1 (e.g. http://host:port/v1) + log_info "Checking models endpoint…" + local models_json + models_json=$(curl "${curl_args[@]}" "${base_url}/models" 2>/dev/null) || { + log_error "Cannot reach inference endpoint at ${base_url}/models" + log_error "Check that the endpoint is running and LUMINA_INFERENCE_BASE_URL is correct." + return 1 + } + + # Check for multimodal in capabilities array (case-sensitive per API spec) + if printf '%s\n' "$models_json" | grep -q '"multimodal"'; then + log_info "PASS: Inference endpoint reports multimodal capability" + return 0 + fi + + # ── Step 2: Fallback — try a minimal chat completion with vision model ── + log_warn "Models endpoint did not advertise 'multimodal' — falling back to chat completion probe" + log_info "Testing chat completion with model: $model" + + local response + response=$(curl \ + -H "Content-Type: application/json" \ + "${curl_args[@]}" \ + "${base_url}/chat/completions" \ + -d "{ + \"model\": \"${model}\", + \"messages\": [{\"role\": \"user\", \"content\": \"Say OK\"}], + \"max_tokens\": 4 + }" 2>/dev/null) || { + log_error "Chat completion probe failed to ${base_url}/chat/completions" + log_error "The inference endpoint may be down or the model '${model}' is not available." + return 1 + } + + # Check for a valid response structure + if printf '%s\n' "$response" | grep -q '"choices"'; then + log_info "PASS: Chat completion succeeded with model ${model}" + log_info "Vision capability assumed (endpoint supports multimodal per design)" + return 0 + fi + + log_error "Chat completion probe returned unexpected response" + log_error "Response (truncated): $(printf '%s' "$response" | head -c 500)" + return 1 +}