diff --git a/dev/macbook-m5pro/README.md b/dev/macbook-m5pro/README.md new file mode 100644 index 0000000..f74bff0 --- /dev/null +++ b/dev/macbook-m5pro/README.md @@ -0,0 +1,280 @@ +# MacBook Pro M5 Pro Azazel Development Lab + +This directory defines the **reference development environment** for validating the new M.I.O. cognitive plane together with Azazel-Knowledge and Azazel-Deception before Raspberry Pi HIL testing. + +## Purpose + +The MacBook lab validates system logic and cross-product contracts first: + +```text +Evidence / Replay + | + v +Azazel-Edge (PR #380 branch) + NOC/SOC evaluators + | + v +M.I.O. cognitive shadow core + hypotheses -> evidence gaps -> read-only evidence -> hypothesis revision + | | + | advisory-only | typed/read-only + v v +Azazel-Knowledge Azazel-Deception +long-term context shadow/replay host + ^ | + +------ observed outcome -+ +``` + +The MacBook lab is **not** a replacement for Raspberry Pi validation. It is used to remove logic/integration defects before Pi-specific testing of Linux networking, systemd, nftables/tc, resource pressure, thermals and real interfaces. + +## Safety boundary + +The lab intentionally does **not**: + +- run the Azazel-Edge installer; +- install/enable systemd services; +- change macOS packet filter or routing; +- invoke nftables, iptables or tc; +- connect M.I.O. directly to Action Arbiter enforcement; +- enable live AZ-06 engagement; +- expose Deception on a non-loopback interface; +- use a public/cloud LLM fallback. + +Azazel-Deception is started with its existing `serve_shadow.py` launcher, which pins `live_execution=disabled`. + +## Host baseline + +Reference host: + +- MacBook Pro with Apple Silicon M5 Pro +- macOS / arm64 +- Python 3.11+ +- Xcode Command Line Tools +- Docker Desktop or compatible Docker runtime (required for Deception compose/HIL-style development tests) +- Ollama (required only for live local-model M.I.O. tests) +- authenticated Git access to private `01rabbit/Azazel-Knowledge` + +Each repository gets its own Python virtual environment because Knowledge and Deception intentionally pin different Azazel-Fabric contract versions. + +## Repository pins + +The bootstrap currently uses: + +- `01rabbit/Azazel-Edge` -> `feat/mio-cognitive-shadow-core` (PR #380) +- `01rabbit/Azazel-Knowledge` -> `main` +- `01rabbit/Azazel-Deception` -> `main` + +Do not switch Edge to `main` until PR #380 or its successor is merged. + +## 1. Preflight + +From this repository branch: + +```bash +cd dev/macbook-m5pro +bash preflight.sh +``` + +Preflight is read-only. It verifies macOS/arm64, Python, Xcode tools, Docker reachability, Ollama reachability and GitHub authentication signals. It installs nothing. + +## 2. Bootstrap repositories and virtual environments + +Default lab root is `~/azazel-m5pro-lab`. + +```bash +bash bootstrap.sh +``` + +Bootstrap: + +1. clones/updates Edge, Knowledge and Deception; +2. creates separate `.venv` environments; +3. installs development dependencies; +4. provisions an isolated Knowledge development database under the lab state directory; +5. creates a scoped `ingest,query,read` Knowledge token; +6. creates a random AZ-06 shadow HMAC key; +7. stores runtime secrets outside every Git repository in `~/azazel-m5pro-lab/runtime.env` with mode `0600`; +8. runs the M.I.O. shadow test set. + +The one-time Knowledge provisioning admin token is redacted from bootstrap output and is not persisted in lab logs. The generated scoped Edge development token is the only token retained in `runtime.env`. + +For the complete Knowledge and Deception unit suites: + +```bash +AZAZEL_FULL_TESTS=1 bash bootstrap.sh +``` + +To pull both initial local models during bootstrap: + +```bash +AZAZEL_PULL_MODELS=1 bash bootstrap.sh +``` + +The configured initial M.I.O. chain is: + +```text +qwen3.5:2b + -> unavailable/failed transport only +qwen3.5:0.8b +``` + +There is no automatic cloud fallback. + +## 3. Start the safe shadow lab + +```bash +bash start-shadow-lab.sh +``` + +The launcher starts the Knowledge API **and its offline worker**. The worker drains local event/flow/reaction/deception-observation spools and can build local behavior state, but it is intentionally started without `--online`, so it performs no scheduled internet feed pulls. + +Default loopback services/processes: + +| Service/process | Endpoint | Authority | +|---|---|---| +| Azazel-Knowledge API | `127.0.0.1:8070` | advisory-only | +| Azazel-Knowledge worker | local state/spool | deterministic writer, offline feeds only | +| Azazel-Deception | `127.0.0.1:8071` | shadow/replay, live disabled | +| Ollama | `127.0.0.1:11434` | M.I.O. inference only | + +Service logs are written under `~/azazel-m5pro-lab/logs/` and lab-managed process IDs under `~/azazel-m5pro-lab/pids/`. + +## 4. Run M.I.O. with the real local model + +With Ollama running and the model present: + +```bash +cd ~/azazel-m5pro-lab/repos/Azazel-Edge +PYTHONPATH=py .venv/bin/python bin/azazel-mio-shadow-replay \ + --fixture tests/fixtures/mio/auth_ambiguity_shadow.json \ + --playbook auth-ambiguity-v1 \ + --endpoint http://127.0.0.1:11434 +``` + +Expected boundary: + +```text +Evidence/evaluator state + -> MioSituationFrame + -> multiple hypotheses + -> evidence gaps + -> read-only Capability Broker + -> evidence-driven hypothesis revision + -> grounded recommendation + -> executable=false +``` + +A M.I.O. recommendation must not change Edge Defensive State in this stage. + +## 5. Model comparison on M5 Pro + +The Mac is the quality/reference environment. Run the same fixture against 0.8B, 2B, and later a larger local comparison model while keeping SituationFrame and Playbook identical. + +Measure separately: + +- hypothesis coverage; +- falsification quality; +- evidence-gap usefulness; +- fabricated-reference rejection; +- recommendation grounding; +- latency; +- peak memory; +- deterministic fallback behavior. + +The Raspberry Pi candidate is selected from measured results, not assumed from parameter count. + +## 6. Knowledge integration target + +Knowledge already supports a local FastAPI surface and persists deterministic/advisory CTI state. The lab also runs the offline worker so event, reaction and deception-observation ingestion can be exercised without a Knowledge appliance. + +The M.I.O. integration must remain: + +```text +M.I.O. -> typed Knowledge query +Knowledge -> context + provenance + freshness + limitations +M.I.O. -> hypothesis/reasoning update +``` + +Never: + +```text +Knowledge -> enforcement command +``` + +The lab's Knowledge state is deliberately isolated from the repository and from any production dataset. + +## 7. Deception integration target + +The development sequence is: + +```text +M.I.O. information objective + -> non-executable Engagement Advisory + -> deterministic Edge mapping + -> Arbiter decision + -> AZ-06 shadow/replay request + -> observed interaction/result + -> M.I.O. hypothesis revision + -> evidence-backed Knowledge feedback +``` + +Until the #373 adversarial gate is passed, development must remain shadow/replay only. The Mac lab must not be used to turn a model recommendation directly into AZ-06 activation. + +## 8. When to move Edge to Raspberry Pi + +Move Azazel-Edge to Raspberry Pi only after the Mac development gate demonstrates: + +- stable M.I.O. reasoning/replay behavior; +- Knowledge absent/slow/malformed/wrong degradation; +- Deception unavailable/malformed/stale degradation; +- cross-trace rejection; +- prompt/delimiter/Unicode injection resistance; +- deterministic Arbiter remains the only authority; +- closed-loop replay is reproducible; +- #373 adversarial review findings are resolved or explicitly accepted. + +Then Raspberry Pi HIL focuses on: + +- Raspberry Pi OS/Linux behavior; +- systemd/service lifecycle; +- real NIC/interface state; +- nftables/tc/enforcement dry-run and approved paths; +- CPU/RAM/swap/temperature; +- long-duration stability; +- 0.8B/2B latency and resource budgets. + +## Directory layout created by bootstrap + +```text +~/azazel-m5pro-lab/ + repos/ + Azazel-Edge/ + Azazel-Knowledge/ + Azazel-Deception/ + state/ + knowledge/ + deception/ + logs/ + pids/ + runtime.env # mode 0600; never commit +``` + +## Customization + +Useful environment variables: + +```bash +AZAZEL_LAB_ROOT=/path/to/lab +AZAZEL_GIT_BASE=git@github.com:01rabbit +AZAZEL_EDGE_BRANCH=feat/mio-cognitive-shadow-core +AZAZEL_FULL_TESTS=1 +AZAZEL_PULL_MODELS=1 +``` + +For SSH Git URLs, set for example: + +```bash +AZAZEL_GIT_BASE=git@github.com:01rabbit bash bootstrap.sh +``` + +The scripts never write runtime secrets into any Azazel Git repository. diff --git a/dev/macbook-m5pro/bootstrap.sh b/dev/macbook-m5pro/bootstrap.sh new file mode 100644 index 0000000..a98b570 --- /dev/null +++ b/dev/macbook-m5pro/bootstrap.sh @@ -0,0 +1,150 @@ +#!/usr/bin/env bash +set -euo pipefail + +LAB_ROOT="${AZAZEL_LAB_ROOT:-$HOME/azazel-m5pro-lab}" +REPOS="$LAB_ROOT/repos" +STATE="$LAB_ROOT/state" +LOGS="$LAB_ROOT/logs" +RUNTIME_ENV="$LAB_ROOT/runtime.env" +GIT_BASE="${AZAZEL_GIT_BASE:-https://github.com/01rabbit}" +EDGE_BRANCH="${AZAZEL_EDGE_BRANCH:-feat/mio-cognitive-shadow-core}" + +mkdir -p "$REPOS" "$STATE" "$LOGS" +chmod 700 "$LAB_ROOT" "$STATE" "$LOGS" 2>/dev/null || true + +"$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/preflight.sh" + +clone_or_update() { + local name="$1" branch="$2" url="$3" dest="$REPOS/$name" + if [[ -d "$dest/.git" ]]; then + echo "[repo] updating $name ($branch)" + git -C "$dest" fetch --prune origin "$branch" + git -C "$dest" checkout "$branch" + git -C "$dest" merge --ff-only "origin/$branch" + else + echo "[repo] cloning $name ($branch)" + git clone --branch "$branch" --single-branch "$url" "$dest" + fi +} + +clone_or_update "Azazel-Edge" "$EDGE_BRANCH" "$GIT_BASE/Azazel-Edge.git" +clone_or_update "Azazel-Knowledge" "main" "$GIT_BASE/Azazel-Knowledge.git" +clone_or_update "Azazel-Deception" "main" "$GIT_BASE/Azazel-Deception.git" + +EDGE="$REPOS/Azazel-Edge" +KNOW="$REPOS/Azazel-Knowledge" +DECEPTION="$REPOS/Azazel-Deception" + +make_venv() { + local repo="$1" + if [[ ! -d "$repo/.venv" ]]; then + python3 -m venv "$repo/.venv" + fi + "$repo/.venv/bin/python" -m pip install --upgrade pip setuptools wheel +} + +make_venv "$EDGE" +"$EDGE/.venv/bin/pip" install -r "$EDGE/requirements/dev.txt" + +make_venv "$KNOW" +( + cd "$KNOW" + .venv/bin/pip install -e '.[api,dev]' +) + +make_venv "$DECEPTION" +( + cd "$DECEPTION" + .venv/bin/pip install -e '.[dev]' +) + +# Load previously generated dev-only secrets before deciding whether they remain valid. +if [[ -f "$RUNTIME_ENV" ]]; then + # shellcheck disable=SC1090 + source "$RUNTIME_ENV" +fi + +KNOW_STATE="$STATE/knowledge" +mkdir -p "$KNOW_STATE" +NEW_KNOW_STATE=0 +if [[ ! -f "$KNOW_STATE/data/db/local.db" ]]; then + NEW_KNOW_STATE=1 + echo "[knowledge] provisioning isolated development state" + provision_out="$( + cd "$KNOW" + AZAZEL_ROOT="$KNOW_STATE" AZAZEL_CONFIG_DIR="$KNOW/config" .venv/bin/python ./azctl provision + )" + # Provision prints a one-time admin token. Do not persist it in lab logs. + printf '%s\n' "$provision_out" | sed -E 's/^( token: ).*$/\1[REDACTED]/' + AZAZEL_KNOWLEDGE_TOKEN="" +fi + +if [[ "$NEW_KNOW_STATE" == "1" || -z "${AZAZEL_KNOWLEDGE_TOKEN:-}" ]]; then + echo "[knowledge] minting a scoped Edge development token (ingest,query,read)" + client_out="$(cd "$KNOW" && AZAZEL_ROOT="$KNOW_STATE" AZAZEL_CONFIG_DIR="$KNOW/config" .venv/bin/python ./azctl client add --scopes ingest,query,read)" + AZAZEL_KNOWLEDGE_TOKEN="$(printf '%s\n' "$client_out" | sed -n 's/^token (shown once): //p' | tail -n 1)" + [[ -n "$AZAZEL_KNOWLEDGE_TOKEN" ]] || { echo "failed to extract Knowledge dev token" >&2; exit 2; } +fi + +AZAZEL_EDGE_ID="${AZAZEL_EDGE_ID:-edge-m5pro-dev}" +AZAZEL_DECEPTION_NODE_ID="${AZAZEL_DECEPTION_NODE_ID:-az06-m5pro-shadow}" +if [[ -z "${AZAZEL_DECEPTION_HMAC_KEY:-}" ]]; then + AZAZEL_DECEPTION_HMAC_KEY="$(python3 - <<'PY' +import secrets +print(secrets.token_hex(32)) +PY +)" +fi +AZAZEL_KNOWLEDGE_PORT="${AZAZEL_KNOWLEDGE_PORT:-8070}" +AZAZEL_DECEPTION_PORT="${AZAZEL_DECEPTION_PORT:-8071}" +AZAZEL_OLLAMA_ENDPOINT="${AZAZEL_OLLAMA_ENDPOINT:-http://127.0.0.1:11434}" + +{ + printf 'AZAZEL_LAB_ROOT=%q\n' "$LAB_ROOT" + printf 'AZAZEL_EDGE_ID=%q\n' "$AZAZEL_EDGE_ID" + printf 'AZAZEL_DECEPTION_NODE_ID=%q\n' "$AZAZEL_DECEPTION_NODE_ID" + printf 'AZAZEL_DECEPTION_HMAC_KEY=%q\n' "$AZAZEL_DECEPTION_HMAC_KEY" + printf 'AZAZEL_KNOWLEDGE_TOKEN=%q\n' "$AZAZEL_KNOWLEDGE_TOKEN" + printf 'AZAZEL_KNOWLEDGE_PORT=%q\n' "$AZAZEL_KNOWLEDGE_PORT" + printf 'AZAZEL_DECEPTION_PORT=%q\n' "$AZAZEL_DECEPTION_PORT" + printf 'AZAZEL_OLLAMA_ENDPOINT=%q\n' "$AZAZEL_OLLAMA_ENDPOINT" +} > "$RUNTIME_ENV" +chmod 600 "$RUNTIME_ENV" + +echo "[test] Azazel-Edge M.I.O. shadow suite" +( + cd "$EDGE" + PYTHONPATH="$EDGE/py" .venv/bin/python -m pytest -q \ + tests/test_mio_cognitive_shadow_core.py \ + tests/test_mio_shadow_runtime_integration.py \ + tests/test_mio_frame_builder_with_evaluators.py \ + tests/test_mio_adversarial_schema.py +) + +if [[ "${AZAZEL_FULL_TESTS:-0}" == "1" ]]; then + echo "[test] Azazel-Knowledge full suite" + (cd "$KNOW" && .venv/bin/python -m pytest -q) + echo "[test] Azazel-Deception full suite" + (cd "$DECEPTION" && .venv/bin/python -m pytest -q) +else + echo "[test] full Knowledge/Deception suites skipped (set AZAZEL_FULL_TESTS=1 to run them)" +fi + +if [[ "${AZAZEL_PULL_MODELS:-0}" == "1" ]]; then + command -v ollama >/dev/null 2>&1 || { echo "AZAZEL_PULL_MODELS=1 but ollama is not installed" >&2; exit 2; } + echo "[model] pulling qwen3.5:2b and qwen3.5:0.8b" + ollama pull qwen3.5:2b + ollama pull qwen3.5:0.8b +fi + +cat <&2; exit 2; } +warn() { echo "[WARN] $*" >&2; } +ok() { echo "[ OK ] $*"; } + +[[ "$(uname -s)" == "Darwin" ]] || fail "macOS is required" +[[ "$(uname -m)" == "arm64" ]] || fail "Apple Silicon arm64 is required (detected: $(uname -m))" + +command -v git >/dev/null 2>&1 || fail "git is required" +command -v python3 >/dev/null 2>&1 || fail "python3 is required" + +python3 - <<'PY' +import sys +if sys.version_info < (3, 11): + raise SystemExit(f"Python >=3.11 is required for the combined lab; found {sys.version.split()[0]}") +print(f"[ OK ] Python {sys.version.split()[0]}") +PY + +if xcode-select -p >/dev/null 2>&1; then + ok "Xcode Command Line Tools: $(xcode-select -p)" +else + fail "Xcode Command Line Tools are required (run: xcode-select --install)" +fi + +if command -v docker >/dev/null 2>&1; then + if docker info >/dev/null 2>&1; then + docker compose version >/dev/null 2>&1 || fail "docker compose plugin is required" + ok "Docker runtime reachable" + else + warn "Docker CLI exists but daemon is not reachable; start Docker Desktop/Colima before Deception tests" + fi +else + warn "Docker is not installed; M.I.O./Knowledge tests work, Deception compose tests will not" +fi + +if command -v ollama >/dev/null 2>&1; then + ok "Ollama CLI found: $(command -v ollama)" + if curl -fsS --max-time 2 http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then + ok "Ollama API reachable on loopback" + else + warn "Ollama is installed but API is not reachable; run: ollama serve" + fi +else + warn "Ollama is not installed; deterministic/replay tests work, live 0.8B/2B inference will not" +fi + +if command -v gh >/dev/null 2>&1; then + if gh auth status >/dev/null 2>&1; then + ok "GitHub CLI authentication available" + else + warn "gh exists but is not authenticated; private Azazel-Knowledge clone/install may fail" + fi +else + warn "GitHub CLI is not installed; ensure git HTTPS/SSH credentials can access private Azazel-Knowledge" +fi + +mem_bytes="$(sysctl -n hw.memsize 2>/dev/null || echo 0)" +if [[ "$mem_bytes" =~ ^[0-9]+$ ]] && (( mem_bytes > 0 )); then + mem_gb=$(( mem_bytes / 1024 / 1024 / 1024 )) + ok "System memory: ${mem_gb} GiB" +fi + +free_kb="$(df -Pk "$HOME" | awk 'NR==2 {print $4}')" +if [[ "$free_kb" =~ ^[0-9]+$ ]]; then + free_gb=$(( free_kb / 1024 / 1024 )) + (( free_gb >= 20 )) || warn "Less than 20 GiB free under HOME (${free_gb} GiB); model images and Docker layers may be tight" + ok "Free space under HOME: ${free_gb} GiB" +fi + +cat <<'EOF' + +MacBook M5 Pro lab preflight complete. +Nothing was installed and no enforcement/network-control path was enabled. +EOF diff --git a/dev/macbook-m5pro/start-shadow-lab.sh b/dev/macbook-m5pro/start-shadow-lab.sh new file mode 100644 index 0000000..59b845f --- /dev/null +++ b/dev/macbook-m5pro/start-shadow-lab.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +set -euo pipefail + +LAB_ROOT="${AZAZEL_LAB_ROOT:-$HOME/azazel-m5pro-lab}" +RUNTIME_ENV="$LAB_ROOT/runtime.env" +[[ -f "$RUNTIME_ENV" ]] || { echo "missing $RUNTIME_ENV; run bootstrap.sh first" >&2; exit 2; } +# shellcheck disable=SC1090 +source "$RUNTIME_ENV" + +REPOS="$LAB_ROOT/repos" +STATE="$LAB_ROOT/state" +LOGS="$LAB_ROOT/logs" +PIDS="$LAB_ROOT/pids" +mkdir -p "$LOGS" "$PIDS" "$STATE/deception" +chmod 700 "$LOGS" "$PIDS" "$STATE" 2>/dev/null || true + +EDGE="$REPOS/Azazel-Edge" +KNOW="$REPOS/Azazel-Knowledge" +DECEPTION="$REPOS/Azazel-Deception" + +for path in "$EDGE" "$KNOW" "$DECEPTION"; do + [[ -d "$path" ]] || { echo "missing repo: $path" >&2; exit 2; } +done + +is_alive() { + local pidfile="$1" + [[ -f "$pidfile" ]] || return 1 + local pid + pid="$(cat "$pidfile" 2>/dev/null || true)" + [[ "$pid" =~ ^[0-9]+$ ]] && kill -0 "$pid" 2>/dev/null +} + +wait_port() { + local host="$1" port="$2" name="$3" + python3 - "$host" "$port" "$name" <<'PY' +import socket, sys, time +host, port, name = sys.argv[1], int(sys.argv[2]), sys.argv[3] +for _ in range(50): + try: + with socket.create_connection((host, port), timeout=0.3): + print(f"[ OK ] {name}: {host}:{port}") + raise SystemExit(0) + except OSError: + time.sleep(0.1) +raise SystemExit(f"{name} did not open {host}:{port}") +PY +} + +KNOW_WORKER_PID="$PIDS/knowledge-worker.pid" +if is_alive "$KNOW_WORKER_PID"; then + echo "[skip] Knowledge worker already running pid=$(cat "$KNOW_WORKER_PID")" +else + echo "[start] Azazel-Knowledge worker (offline; no feed network pulls)" + ( + cd "$KNOW" + AZAZEL_ROOT="$STATE/knowledge" \ + AZAZEL_CONFIG_DIR="$KNOW/config" \ + nohup .venv/bin/python -m azazel_knowledge.worker \ + --root "$STATE/knowledge" \ + --config-dir "$KNOW/config" \ + --interval-s 1 \ + >"$LOGS/knowledge-worker.log" 2>&1 & + echo $! > "$KNOW_WORKER_PID" + ) + sleep 0.3 + is_alive "$KNOW_WORKER_PID" || { echo "Knowledge worker exited; inspect $LOGS/knowledge-worker.log" >&2; exit 2; } +fi + +KNOW_API_PID="$PIDS/knowledge-api.pid" +if is_alive "$KNOW_API_PID"; then + echo "[skip] Knowledge API already running pid=$(cat "$KNOW_API_PID")" +else + echo "[start] Azazel-Knowledge API on 127.0.0.1:$AZAZEL_KNOWLEDGE_PORT" + ( + cd "$KNOW" + AZAZEL_ROOT="$STATE/knowledge" \ + AZAZEL_CONFIG_DIR="$KNOW/config" \ + nohup .venv/bin/uvicorn azazel_knowledge.api.app:app \ + --host 127.0.0.1 --port "$AZAZEL_KNOWLEDGE_PORT" \ + >"$LOGS/knowledge-api.log" 2>&1 & + echo $! > "$KNOW_API_PID" + ) +fi +wait_port 127.0.0.1 "$AZAZEL_KNOWLEDGE_PORT" "Azazel-Knowledge API" + +DECEPTION_PID="$PIDS/deception.pid" +if is_alive "$DECEPTION_PID"; then + echo "[skip] Deception shadow already running pid=$(cat "$DECEPTION_PID")" +else + echo "[start] Azazel-Deception SHADOW server on 127.0.0.1:$AZAZEL_DECEPTION_PORT" + ( + cd "$DECEPTION" + nohup .venv/bin/python scripts/dev/serve_shadow.py \ + --host 127.0.0.1 \ + --port "$AZAZEL_DECEPTION_PORT" \ + --key "$AZAZEL_DECEPTION_HMAC_KEY" \ + --edge-id "$AZAZEL_EDGE_ID" \ + --node-id "$AZAZEL_DECEPTION_NODE_ID" \ + --state-root "$STATE/deception" \ + >"$LOGS/deception-shadow.log" 2>&1 & + echo $! > "$DECEPTION_PID" + ) +fi +wait_port 127.0.0.1 "$AZAZEL_DECEPTION_PORT" "Azazel-Deception shadow" + +if curl -fsS --max-time 2 "$AZAZEL_OLLAMA_ENDPOINT/api/tags" >/dev/null 2>&1; then + echo "[ OK ] Ollama: $AZAZEL_OLLAMA_ENDPOINT" +else + echo "[WARN] Ollama is not reachable at $AZAZEL_OLLAMA_ENDPOINT; deterministic services are ready, live M.I.O. inference is not" >&2 +fi + +cat <