Implement install stages S0–S2: bootstrap, env, model and vision smoke.

Operator can run make bootstrap/install through S2 using nemohermes/openshell wrappers; docs and implement queue updated. No S3+ and no push.
This commit is contained in:
Ty
2026-07-27 11:47:34 -07:00
parent 998e32e871
commit e5e179e541
14 changed files with 893 additions and 60 deletions
+30
View File
@@ -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
+31 -9
View File
@@ -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. # 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: help:
@echo "Salon_Assistant (Lumina) — structure phase (0.1.0-design)" @echo "Salon_Assistant (Lumina) — $(shell cat VERSION 2>/dev/null || echo 'unknown')"
@echo " Implementation not started. Explicit build/implement required."
@echo "" @echo ""
@echo "Intended targets (after build):" @echo "Implemented targets:"
@echo " make bootstrap - host prereqs (Docker if missing)" @echo " make bootstrap - host prereqs (Docker if missing) [S0b]"
@echo " make install - full install S0bS6" @echo " make install - full install S0bS2"
@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 upgrade - product upgrade"
@echo " make doctor - health checks" @echo " make doctor - health checks"
@echo " make verify - lint + tests + smoke (fixtures)" @echo " make verify - lint + tests + smoke (fixtures)"
@echo " make sync-design - list design pack paths" @echo " make sync-design - list design pack paths"
bootstrap install upgrade doctor verify: # ── Implemented targets ────────────────────────────────────────────────────
@echo "not implemented — wait for explicit build/implement order" >&2; exit 1
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: sync-design:
@echo "Design SSOT:" @echo "Design SSOT:"
+28
View File
@@ -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.
```
+37 -11
View File
@@ -1,29 +1,55 @@
# Deployer host baselining # Deployer host baselining
**Status:** Outline from approved design POR. Procedural scripts at **build**. **Status:** Procedural — ready for operator use.
## Purpose ## Purpose
Create a clean host that can run the Docker-based Lumina stack. This product is **repo-driven**, not tied to one developers 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 | | Item | Spec |
|------|------| |------|------|
| OS | Linux x86_64 or arm64 (cloud VM, bare metal; WSL2 optional path only) | | OS | Linux x86_64 or arm64 (cloud VM, bare metal; WSL2 optional path only) |
| Resources | Multi-core CPU; ≥816 GB RAM recommended; disk for Docker images | | Resources | Multi-core CPU; ≥816 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 | | 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) | | 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). 1. **Provision host** cloud console, hypervisor, or bare metal.
2. Install base OS updates. 2. **Install base OS updates**`sudo apt update && sudo apt upgrade -y` (Debian/Ubuntu) or equivalent.
3. Create deploy user; clone this repository. 3. **Create deploy user** — a non-root user with sudo access.
4. Run `./scripts/bootstrap.sh` (installs Docker if missing — when implemented). 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. Continue with [INSTALL.md](INSTALL.md). 5. **Clone this repository**`git clone <repo-url> && 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 S1S2.
## Design reference ## 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.
+72 -8
View File
@@ -1,13 +1,77 @@
# Hermes model configuration # Hermes model configuration
**Status:** Outline from approved design. **Status:** S2-ready — sufficient for operators configuring models.
| Slot | Requirement | ## Model slots
|------|-------------|
| **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 |
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 <sandbox-name> 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)
+99 -15
View File
@@ -1,31 +1,115 @@
# Install # Install
**Status:** Outline from approved design. Scripts stubbed until **build**. **Status:** Stages S0S2 implemented. S3S7 pending.
## Stages ## Stages
| Stage | Where | Outcome | | Stage | Where | Outcome | Status |
|-------|--------|---------| |-------|--------|---------|--------|
| S0 | Human | Host per [DEPLOYER_HOST.md](DEPLOYER_HOST.md) | | S0 | Human | Host per [DEPLOYER_HOST.md](DEPLOYER_HOST.md) | ✅ Procedural |
| S0b | Host script | Docker installed if missing | | S0b | Host script | Docker installed if missing | ✅ Implemented |
| S1 | Host script | Repo env, `.env` from `.env.example` | | S1 | Host script | Repo env, `.env` from `.env.example` | ✅ Implemented |
| S2 | Host script | Main + aux vision config; vision smoke | | S2 | Host script | Main + aux vision config; vision smoke | ✅ Implemented |
| S3S5 | Host → Compose / `nemohermes` | Stack, sandbox, policy, skills | | S3S5 | Host → Compose / `nemohermes` | Stack, sandbox, policy, skills | ⏳ Pending |
| S6 | Host script | Doctor green | | S6 | Host script | Doctor green | ⏳ Pending |
| S7 | Owner + operator connect helpers | Name assistant; connect **their** SaaS/channels | | S7 | Owner + operator connect helpers | Name assistant; connect **their** SaaS/channels | ⏳ Pending |
## Platform commands (normative) ## Platform commands (normative)
All mutations: `nemohermes` / `openshell` (see [design/research/platform-notes.md](../design/research/platform-notes.md)). 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 ```bash
./scripts/bootstrap.sh ./scripts/bootstrap.sh
./scripts/install.sh # or
./scripts/doctor.sh 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) (S1S5). **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 (S0bS2)
```bash
./scripts/install.sh
# or
make install
# or
make install-s0-s2
```
## After install (S0S2)
- 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) (S1S5) 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.
+43 -13
View File
@@ -1,19 +1,49 @@
# Host scripts (scaffold) # Host scripts
**Status:** Documented entrypoints only — no executable bodies until **build**. **Status:** S0bS2 implemented. S3S7 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 | | Script | Role | Status |
|--------|------| |--------|------|--------|
| `bootstrap.sh` | Host prereqs; install Docker if missing | | `bootstrap.sh` | Host prereqs; install Docker if missing | ✅ S0b |
| `install.sh` | Stages S0bS6 | | `install.sh` | Staged installer (S0bS2) | ✅ S0bS2 |
| `upgrade.sh` | Snapshot, pull pins, migrate, re-apply policy, doctor | | `install/s1-env.sh` | S1: create/validate `.env` | ✅ S1 |
| `doctor.sh` | Health checks | | `install/s2-models.sh` | S2: model + vision config + smoke | ✅ S2 |
| `connect/*.sh` | Operator connect helpers (Square, QBO, Vagaro, channels) | | `upgrade.sh` | Snapshot, pull pins, migrate, re-apply policy, doctor | ⏳ Pending |
| `install/` | Stage helpers | | `doctor.sh` | Health checks | ⏳ Pending |
| `lib/` | Shared shell helpers | | `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 (S0bS2)
./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). See [docs/INSTALL.md](../docs/INSTALL.md), [docs/UPGRADE.md](../docs/UPGRADE.md), [design/updates-lifecycle.md](../design/updates-lifecycle.md).
+97
View File
@@ -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
+109
View File
@@ -0,0 +1,109 @@
#!/usr/bin/env bash
# scripts/install.sh — Lumina staged installer
#
# Runs install stages S0bS2 (S3+ not yet implemented).
#
# Usage:
# ./scripts/install.sh # run all implemented stages (S0bS2)
# ./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 <<EOF
Usage: $(basename "$0") [OPTIONS]
Run Lumina install stages (S0bS2 implemented).
Options:
--stage <s1|s2> 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 S0bS2)"
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 (S0bS2)"
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"
+29
View File
@@ -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."
+87
View File
@@ -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."
+81
View File
@@ -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
}
+74
View File
@@ -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
}
+72
View File
@@ -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
}