#!/bin/bash # Canonical provider installer. coordinator/api/install.sh is generated byte-for-byte # by scripts/sync-install-embed.sh and embedded in the coordinator binary. set -euo pipefail # DICOMPUTE Provider Installer (Swift CLI release v0.5.0+) # Usage: curl -fsSL https://api.dicompute.ai/install.sh | bash # # This script: # 1. Fetches the latest signed release from the coordinator # 2. Downloads the provider app (binaries, metallib, SwiftPM resources) # 3. Verifies bundle SHA-256 + Apple Developer ID code signature # 4. Sets up the Secure Enclave identity # 5. Optionally enrolls in MDM (device attestation) # 6. Optionally downloads a starter model # # Zero prerequisites — just macOS 14+ on Apple Silicon. The Swift CLI # links mlx-swift directly and ships a colocated mlx.metallib for Metal # kernels; there is no Python interpreter to install and no inference # subprocess to spawn. # This source intentionally retains the placeholder used by the coordinator. # End users fetch /install.sh from a coordinator, which substitutes its own base # URL. Direct source execution must set COORD_URL explicitly. COORD_URL="${COORD_URL:-https://api.dicompute.ai}" INSTALL_DIR="$HOME/.darkbloom" BIN_DIR="$INSTALL_DIR/bin" DARKBLOOM_DESIGNATED_REQUIREMENT='anchor apple generic and identifier "io.darkbloom.provider" and certificate leaf[subject.OU] = "SLDQ2GJ6TL"' DARKBLOOM_FAN_HELPER_REQUIREMENT='anchor apple generic and identifier "io.darkbloom.fan-helper" and certificate leaf[subject.OU] = "SLDQ2GJ6TL"' FAN_HELPER_REQUIREMENT="$DARKBLOOM_FAN_HELPER_REQUIREMENT" INSTALL_TEST_MODE=0 fail_install() { echo " ✗ $*" >&2 return 1 } verify_file_hash() { local file=$1 local expected=$2 local label=$3 [ -z "$expected" ] && return 0 local actual actual=$(shasum -a 256 "$file" | cut -d' ' -f1) [ "$actual" = "$expected" ] \ || fail_install "$label hash mismatch (expected $expected, got $actual)." } verify_code_requirement() { local target=$1 local deep=$2 local requirement=$3 if [ "$deep" = "1" ]; then codesign --verify --deep --strict --verbose=2 \ "-R=$requirement" "$target" >/dev/null 2>&1 else codesign --verify --strict --verbose=2 \ "-R=$requirement" "$target" >/dev/null 2>&1 fi } verify_staged_app_signature() { local app=$1 local requirement=${2:-$DARKBLOOM_DESIGNATED_REQUIREMENT} verify_code_requirement "$app" 1 "$requirement" || { fail_install "Staged Darkbloom.app does not satisfy the pinned signature requirement." return 1 } } # Stock-macOS binary capability probe. `strings` is an Xcode CLT shim on a # pristine Mac (it prompts/fails without developer tools), so scan the file # directly with BSD grep's binary-as-text mode — grep ships in base macOS. binary_contains_paged_code() { local binary=$1 LC_ALL=C grep -a -q -F 'engine_v2_kv_backend' "$binary" } verify_fan_helper_capability() { local app=$1 local executable="$app/Contents/MacOS/darkbloom" local marker="$app/Contents/Resources/darkbloom-runtime-capabilities/fan-helper-v1" local helper="$app/Contents/Helpers/darkbloom-fan-helper" local code_present=0 local marker_present=0 local helper_present=0 LC_ALL=C grep -a -q -F 'darkbloom-fan-helper-v1' "$executable" \ && code_present=1 if [ -e "$marker" ] || [ -L "$marker" ]; then marker_present=1; fi if [ -e "$helper" ] || [ -L "$helper" ]; then helper_present=1; fi [ "$code_present" -eq "$marker_present" ] \ && [ "$marker_present" -eq "$helper_present" ] || { fail_install "Fan-helper CLI capability, marker, and nested helper must be present together." return 1 } [ "$marker_present" -eq 1 ] || return 0 [ -f "$marker" ] && [ ! -L "$marker" ] \ && [ "$(tr -d '[:space:]' < "$marker")" = "1" ] || { fail_install "Fan-helper capability marker is invalid." return 1 } [ -f "$helper" ] && [ ! -L "$helper" ] && [ -x "$helper" ] || { fail_install "Bundled fan helper must be a regular executable, not a symlink." return 1 } [ "$(stat -f '%Lp' "$helper" 2>/dev/null || true)" = "755" ] || { fail_install "Bundled fan helper must have mode 0755." return 1 } verify_code_requirement "$helper" 0 "$FAN_HELPER_REQUIREMENT" || { fail_install "Bundled fan helper does not satisfy the pinned helper signature requirement." return 1 } } verify_staged_app() { local app=$1 local executable="$app/Contents/MacOS/darkbloom" local marker="$app/Contents/Resources/darkbloom-runtime-capabilities/paged-kernel-v1" if [ "$INSTALL_TEST_MODE" = "1" ]; then codesign --verify --deep --strict --verbose=2 "$app" >/dev/null 2>&1 || { fail_install "Strict code-signature verification failed for staged Darkbloom.app." return 1 } else verify_staged_app_signature "$app" || return 1 fi verify_fan_helper_capability "$app" || return 1 local code_has_paged=0 local marker_present=0 binary_contains_paged_code "$executable" && code_has_paged=1 [ -f "$marker" ] && marker_present=1 [ "$code_has_paged" -eq "$marker_present" ] || { if [ "$code_has_paged" -eq 1 ]; then fail_install "Paged-capable staged app is missing its signed capability marker." else fail_install "Staged app advertises paged capability without paged runtime code." fi return 1 } [ "$marker_present" -eq 1 ] || return 0 [ "$(tr -d '[:space:]' < "$marker")" = "1" ] || { fail_install "Paged runtime capability marker is invalid." return 1 } shopt -s nullglob local paged_resources=( "$app/Contents/Resources"/*.bundle/pagedattention.metal ) local expected_resource="$app/Contents/Resources/mlx-swift-lm_MLXLMCommon.bundle/pagedattention.metal" [ "${#paged_resources[@]}" -eq 1 ] \ && [ "${paged_resources[0]}" = "$expected_resource" ] \ && [ -s "$expected_resource" ] \ || { fail_install "Paged-capable staged app requires exactly one sealed MLXLMCommon pagedattention.metal." return 1 } DARKBLOOM_NO_UPDATE_CHECK=1 "$executable" runtime-smoke >/dev/null \ || { fail_install "Packaged paged-kernel runtime smoke failed." return 1 } } verify_staged_app_payload() { local app=$1 local binary_hash=$2 local metallib_hash=$3 local app_bin="$app/Contents/MacOS" [ -n "$binary_hash" ] && [ -n "$metallib_hash" ] || { fail_install "App releases require binary_hash and metallib_hash." return 1 } verify_file_hash "$app_bin/darkbloom" "$binary_hash" "App binary" \ && verify_file_hash "$app_bin/mlx.metallib" "$metallib_hash" "App metallib" } commit_staged_app() { local staged_app=$1 local install_dir=$2 local backup="$install_dir/.install-backup-$$-$RANDOM" local destination="$install_dir/Darkbloom.app" local had_previous=0 mkdir -p "$backup" "$install_dir/bin" if [ -d "$destination" ]; then mv "$destination" "$backup/Darkbloom.app" || { rm -rf "$backup" return 1 } had_previous=1 fi if ! mv "$staged_app" "$destination"; then [ "$had_previous" -eq 1 ] \ && mv "$backup/Darkbloom.app" "$destination" 2>/dev/null || true rm -rf "$backup" return 1 fi local app_bin="$destination/Contents/MacOS" if ! ln -sfn "../Darkbloom.app/Contents/MacOS/darkbloom" "$install_dir/bin/darkbloom" \ || ! ln -sfn "../Darkbloom.app/Contents/MacOS/darkbloom-enclave" "$install_dir/bin/darkbloom-enclave" \ || ! ln -sfn "../Darkbloom.app/Contents/MacOS/mlx.metallib" "$install_dir/bin/mlx.metallib" \ || ! ln -sfn "darkbloom-enclave" "$install_dir/bin/eigeninference-enclave" then rm -rf "$destination" [ "$had_previous" -eq 1 ] \ && mv "$backup/Darkbloom.app" "$destination" 2>/dev/null || true rm -rf "$backup" return 1 fi chmod +x "$app_bin/darkbloom" "$app_bin/darkbloom-enclave" rm -rf "$backup" } commit_staged_flat_bundle() { local staged_bin=$1 local install_dir=$2 local backup="$install_dir/.install-backup-$$-$RANDOM" local destination="$install_dir/bin" mkdir -p "$backup" if [ -d "$destination" ]; then mv "$destination" "$backup/bin" || { rm -rf "$backup" return 1 } fi if ! mv "$staged_bin" "$destination"; then [ -d "$backup/bin" ] && mv "$backup/bin" "$destination" 2>/dev/null || true rm -rf "$backup" return 1 fi chmod +x "$destination/darkbloom" "$destination/darkbloom-enclave" ln -sfn "darkbloom-enclave" "$destination/eigeninference-enclave" rm -rf "$backup" } install_bundle_atomically() { local archive=$1 local install_dir=$2 local binary_hash=${3:-} local metallib_hash=${4:-} local stage="$install_dir/.install-staging-$$-$RANDOM" rm -rf "$stage" mkdir -p "$stage" if ! tar xzf "$archive" -C "$stage"; then rm -rf "$stage" return 1 fi local flat_bin="$stage/bin" [ -f "$flat_bin/darkbloom" ] \ && [ -f "$flat_bin/darkbloom-enclave" ] \ && [ -f "$flat_bin/mlx.metallib" ] \ || { rm -rf "$stage" fail_install "Release bundle is missing required flat verifier files." return 1 } verify_file_hash "$flat_bin/darkbloom" "$binary_hash" "Binary" || { rm -rf "$stage" return 1 } verify_file_hash "$flat_bin/mlx.metallib" "$metallib_hash" "Metallib" || { rm -rf "$stage" return 1 } if [ -d "$stage/Darkbloom.app" ]; then verify_staged_app_payload \ "$stage/Darkbloom.app" "$binary_hash" "$metallib_hash" || { rm -rf "$stage" return 1 } verify_staged_app "$stage/Darkbloom.app" || { rm -rf "$stage" return 1 } commit_staged_app "$stage/Darkbloom.app" "$install_dir" || { rm -rf "$stage" fail_install "Atomic app swap failed; previous install was restored." return 1 } else if [ "$INSTALL_TEST_MODE" = "1" ]; then codesign --verify --strict --verbose=2 "$flat_bin/darkbloom" >/dev/null 2>&1 || { rm -rf "$stage" fail_install "Strict signature verification failed for legacy flat artifact." return 1 } else verify_code_requirement \ "$flat_bin/darkbloom" 0 "$DARKBLOOM_DESIGNATED_REQUIREMENT" || { rm -rf "$stage" fail_install "Legacy flat artifact does not satisfy the pinned signature requirement." return 1 } fi commit_staged_flat_bundle "$flat_bin" "$install_dir" || { rm -rf "$stage" fail_install "Atomic flat-bundle swap failed; previous install was restored." return 1 } fi rm -rf "$stage" } if [ "${1:-}" = "--verify-staged-app-signature-test" ]; then [ "$#" -eq 3 ] || { echo "usage: $0 --verify-staged-app-signature-test " >&2 exit 64 } verify_staged_app_signature "$2" "$3" exit $? fi if [ "${1:-}" = "--install-bundle-test" ]; then { [ "$#" -eq 5 ] || [ "$#" -eq 6 ]; } || { echo "usage: $0 --install-bundle-test [fan-helper-requirement]" >&2 exit 64 } INSTALL_TEST_MODE=1 if [ "$#" -eq 6 ]; then FAN_HELPER_REQUIREMENT=$6; fi install_bundle_atomically "$2" "$3" "$4" "$5" exit $? fi # Detect interactive vs piped (curl | bash). if [ -t 0 ]; then INTERACTIVE=true else INTERACTIVE=false fi # ─── PATH integration ─────────────────────────────────────────────────── # Ensure $1 is on the user's PATH so the installed command is found in future # shells. Appends an idempotent export to the user's shell rc file(s) and # exports it for the current process. Fixes issue #198 ("command not found # after install" — typically ~/.local/bin not on PATH on Ubuntu). ensure_on_path() { local dir="$1" [ -n "$dir" ] || return 0 # Already on PATH? nothing to do. case ":$PATH:" in *":$dir:"*) export PATH="$PATH"; return 0 ;; esac export PATH="$dir:$PATH" # current shell local line="export PATH=\"$dir:\$PATH\" # added by DICOMPUTE install.sh" local rc for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do # Only touch rc files that exist OR the always-sourced .profile; append # once (idempotent — skip if the exact dir export is already present). if [ -f "$rc" ] || [ "$rc" = "$HOME/.profile" ]; then if ! grep -qsF "PATH=\"$dir:" "$rc" 2>/dev/null; then printf '\n%s\n' "$line" >> "$rc" 2>/dev/null || true fi fi done } # ─── Release fetch (distinguishes 404 from network failure) ────────────── # Fetches a release JSON from the coordinator and, on failure, prints an # ACCURATE message: a 404 means no release row is registered for the platform # (issue #271), NOT a network problem. Prior code used `curl -fsSL ... || echo ""` # which collapsed both cases into a misleading "check your internet" error. # Usage: fetch_release "" "" → prints JSON on stdout, or # returns non-zero after printing the diagnostic to stderr. fetch_release() { local url="$1" label="$2" body status # -s silent, -S show real errors, -L follow, -w capture status; NO -f so we # can read the 404 body/status instead of curl exiting empty. body=$(curl -sSL -w '\n%{http_code}' "$url" 2>/dev/null) || { echo " ✗ Could not reach coordinator at $COORD_URL" >&2 echo " Check your internet connection and try again." >&2 return 1 } status=$(printf '%s' "$body" | tail -n1) body=$(printf '%s' "$body" | sed '$d') # strip the trailing status line case "$status" in 200) printf '%s' "$body"; return 0 ;; 404) echo " ✗ No active release for ${label} (coordinator returned 404)." >&2 echo " The coordinator is reachable, but no ${label} release is registered yet." >&2 echo " A signed ${label} bundle must be published (release-swift.yml / POST /v1/releases)." >&2 return 2 ;; *) echo " ✗ Coordinator returned HTTP ${status} fetching the ${label} release." >&2 return 1 ;; esac } # ─── Portable provider (non-Apple-Silicon) ────────────────────────────── # Apple Silicon runs the verified MLX provider (the rest of this script). # Linux hosts install the Go connector (dico-provider) — binary, no Python. # Intel Macs fall back to the Python+llama.cpp tier (no dico-provider binary # for darwin/amd64 yet). dico_portable_install() { # ── Linux: Go connector path ───────────────────────────────────────── if [ "$(uname)" = "Linux" ]; then dico_linux_install return $? fi # ── Non-Linux, non-Apple-Silicon (Intel Mac): Python fallback ──────── echo "╔══════════════════════════════════════════════╗" echo "║ DICOMPUTE — Portable Provider (llama.cpp) ║" echo "╚══════════════════════════════════════════════╝" echo "" echo "Intel Mac — joining via the portable CPU tier (llama.cpp + Python)." echo "" local DIR="$HOME/.dicompute/provider" local MODELS="$DIR/models" local VENV="$DIR/venv" local PORT="${DICO_LLAMA_PORT:-18091}" local MODEL_ID="${DICO_MODEL:-qwen2.5-3b-instruct}" local GGUF_URL="${DICO_MODEL_URL:-https://huggingface.co/Qwen/Qwen2.5-3B-Instruct-GGUF/resolve/main/qwen2.5-3b-instruct-q4_k_m.gguf?download=true}" local GGUF="$MODELS/$(basename "${GGUF_URL%%\?*}")" local NICE="${DICO_NICE:-10}" local THREADS_FLAG="" if [ -n "${DICO_THREADS:-}" ]; then THREADS_FLAG="--threads ${DICO_THREADS}"; fi local WS="${COORD_URL/https:/wss:}"; WS="${WS/http:/ws:}/ws/provider" mkdir -p "$MODELS" local CHIP MEMGB CORES MACHINE CHIP=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo "CPU") MEMGB=$(sysctl -n hw.memsize 2>/dev/null | awk '{printf "%.0f",$1/1073741824}') CORES=$(sysctl -n hw.ncpu 2>/dev/null || echo 8) MACHINE=$(sysctl -n hw.model 2>/dev/null || echo "Mac") echo "→ [1/6] Checking prerequisites ..." if ! command -v python3 >/dev/null 2>&1; then echo " ✗ python3 is required. brew install python" return 1 fi echo " ✓ python3 $(python3 --version 2>&1 | awk '{print $2}') · ${CHIP} · ${MEMGB}GB RAM" echo "→ [2/6] Ensuring llama-server (llama.cpp) ..." local LLAMA_BIN="" if command -v llama-server >/dev/null 2>&1; then LLAMA_BIN="$(command -v llama-server)"; echo " ✓ already on PATH" elif [ -x "$DIR/llama/llama-server" ]; then LLAMA_BIN="$DIR/llama/llama-server"; echo " ✓ already installed locally" else # NOTE: the unpinned GitHub 'latest' release fetch was removed (security: # supply-chain / hash mismatch risk). llama-server is now fetched by the # connector itself via GET /v1/engines/latest which returns a coordinator- # pinned URL + SHA-256. The connector's ensureEngine() handles this on # first start. Install from Homebrew as a convenience fallback only. if command -v brew >/dev/null 2>&1; then HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1 brew install llama.cpp 2>/dev/null || true command -v llama-server >/dev/null 2>&1 && LLAMA_BIN="$(command -v llama-server)" fi if [ -z "$LLAMA_BIN" ]; then echo " ✓ llama-server not found on PATH — connector will fetch a pinned build on first start" LLAMA_BIN="(fetched on start)" fi fi [ "$LLAMA_BIN" = "(fetched on start)" ] || echo " ✓ llama-server: $LLAMA_BIN" echo "→ [3/6] Setting up Python environment ..." [ -d "$VENV" ] || python3 -m venv "$VENV" "$VENV/bin/pip" install -q --disable-pip-version-check websockets pynacl || { echo " ✗ pip install failed."; return 1; } echo " ✓ environment ready" echo "→ [4/6] Fetching provider connector ..." curl -fsSL "$COORD_URL/provider.py" -o "$DIR/provider.py" || { echo " ✗ could not fetch $COORD_URL/provider.py"; return 1; } echo " ✓ connector installed" if [ -f "$GGUF" ]; then echo "→ [5/6] Model already present ($(du -h "$GGUF" 2>/dev/null | cut -f1)) ✓" else echo "→ [5/6] Downloading model $MODEL_ID ..." curl -fL -C - --progress-bar -o "$GGUF" "$GGUF_URL" || { echo " ✗ model download failed."; return 1; } echo " ✓ model downloaded" fi cat > "$DIR/start.sh" </dev/null 2>&1; then nohup nice -n $NICE "$LLAMA_BIN" -m "$GGUF" --host 127.0.0.1 --port $PORT -c 4096 $THREADS_FLAG > "$DIR/llama.log" 2>&1 & for i in \$(seq 1 40); do curl -fsS "http://127.0.0.1:$PORT/health" >/dev/null 2>&1 && break; sleep 1; done fi export DICO_WS="$WS" export DICO_LLAMA="http://127.0.0.1:$PORT/v1/chat/completions" export DICO_MODEL="$MODEL_ID" [ -f "$DIR/token" ] && export DICO_TOKEN="\$(cat "$DIR/token")" exec "$VENV/bin/python" "$DIR/provider.py" LAUNCH chmod +x "$DIR/start.sh" # Install a `dicompute` command on PATH so the portable provider is # controllable by name (issue #198 — the Python tier previously left # nothing on PATH). Thin wrapper over start.sh / link.sh. local WBIN="$HOME/.local/bin" mkdir -p "$WBIN" 2>/dev/null || true cat > "$WBIN/dicompute" < "\$DIR/provider.log" 2>&1 & ;; link|login) exec "\$DIR/link.sh" "\${@:2}" 2>/dev/null || exec "\$DIR/start.sh" ;; stop) pkill -f provider.py; pkill -f llama-server; echo "stopped" ;; logs) exec tail -f "\$DIR/provider.log" ;; status) grep -q registered "\$DIR/provider.log" 2>/dev/null && echo "registered" || echo "not registered" ;; *) echo "usage: dicompute {start|stop|logs|status|login}"; exit 1 ;; esac WRAP chmod +x "$WBIN/dicompute" 2>/dev/null || true ln -sf "$WBIN/dicompute" "$WBIN/dico-provider" 2>/dev/null || true ensure_on_path "$WBIN" echo "" echo "→ [6/6] Starting llama-server + connecting to the coordinator ..." : > "$DIR/provider.log" nohup "$DIR/start.sh" > "$DIR/provider.log" 2>&1 & local ok=0 i for i in $(seq 1 60); do if grep -q "registered" "$DIR/provider.log" 2>/dev/null; then ok=1; break; fi if grep -qiE "FATAL|Traceback|Error:" "$DIR/provider.log" 2>/dev/null; then break; fi printf "."; sleep 2 done printf "\n\n" if [ "$ok" = "1" ]; then echo "✓ Connected and registered as a DICOMPUTE provider." else echo "⚠ Not registered yet — check logs:" tail -n 10 "$DIR/provider.log" 2>/dev/null | sed 's/^/ /' echo " Watch live: tail -f $DIR/provider.log" fi echo "" echo " Command: dicompute {start|stop|logs|status} (open a NEW terminal, or run: export PATH=\"$WBIN:\$PATH\")" echo " Logs: $DIR/provider.log (llama: $DIR/llama.log)" echo " Restart: dicompute start (or $DIR/start.sh)" echo " Stop: dicompute stop" return 0 } # ─── Linux Go connector installer ─────────────────────────────────────────── # Downloads the dico-provider Go binary from the coordinator's release endpoint, # verifies SHA-256, and runs 'dico-provider setup' to install the systemd unit. # # TODO(CI): the release workflow must publish dico-provider-linux-amd64.tar.gz # and dico-provider-linux-arm64.tar.gz to R2 and POST /v1/releases with # platform=linux-amd64 (or linux-arm64) before this path is fully live. dico_linux_install() { echo "╔══════════════════════════════════════════════╗" echo "║ DICOMPUTE — Linux Provider (dico-provider) ║" echo "╚══════════════════════════════════════════════╝" echo "" local DIR="$HOME/.dicompute/provider" local BIN_DIR="$HOME/.dicompute/bin" local ARCH="" case "$(uname -m)" in x86_64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; *) echo " ✗ Unsupported architecture: $(uname -m)" return 1 ;; esac local PLATFORM="linux-${ARCH}" local WS="${COORD_URL/https:/wss:}"; WS="${WS/http:/ws:}/ws/provider" mkdir -p "$DIR" "$BIN_DIR" echo "→ [1/4] Fetching latest release for $PLATFORM ..." local RELEASE_JSON="" fr_rc=0 # fetch_release distinguishes 404 (no release row) from a real network error # and prints the accurate diagnostic to stderr (issue #271). RELEASE_JSON=$(fetch_release "$COORD_URL/v1/releases/latest?platform=$PLATFORM" "$PLATFORM") || fr_rc=$? if [ "$fr_rc" -eq 1 ]; then return 1 # genuine network / non-404 error, already reported fi local BUNDLE_URL="" BUNDLE_HASH="" BINARY_HASH="" VERSION="" BUNDLE_URL=$(printf '%s' "$RELEASE_JSON" | sed -n 's/.*"url":"\([^"]*\)".*/\1/p') BUNDLE_HASH=$(printf '%s' "$RELEASE_JSON" | sed -n 's/.*"bundle_hash":"\([^"]*\)".*/\1/p') BINARY_HASH=$(printf '%s' "$RELEASE_JSON" | sed -n 's/.*"binary_hash":"\([^"]*\)".*/\1/p') VERSION=$(printf '%s' "$RELEASE_JSON" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p') if [ -z "$BUNDLE_URL" ] || [ -z "$BUNDLE_HASH" ]; then echo " ✗ No dico-provider release found for $PLATFORM." echo " This is a new platform — the release CI step must publish the binary first." echo " Until then, build from source or use the Python connector:" echo " git clone https://github.com/DICOMPUTE/DiCo" echo " cd DiCo && GOOS=linux GOARCH=$ARCH CGO_ENABLED=0 go build -o dico-provider ./coordinator/cmd/dico-provider" echo " ./dico-provider setup" return 1 fi echo " Version: $VERSION" echo "→ [2/4] Downloading dico-provider bundle ..." local TARBALL="$DIR/dico-provider-update.tar.gz" curl -fL --progress-bar -o "$TARBALL" "$BUNDLE_URL" || { echo " ✗ download failed."; return 1; } echo "→ [3/4] Verifying SHA-256 ..." local ACTUAL_HASH="" ACTUAL_HASH=$(sha256sum "$TARBALL" 2>/dev/null | cut -d' ' -f1 \ || shasum -a 256 "$TARBALL" 2>/dev/null | cut -d' ' -f1 || echo "") if [ -z "$ACTUAL_HASH" ] || [ "$ACTUAL_HASH" != "$BUNDLE_HASH" ]; then echo " ✗ Bundle hash mismatch — refusing to install." echo " Expected: $BUNDLE_HASH" echo " Got: $ACTUAL_HASH" rm -f "$TARBALL" return 1 fi echo " ✓ Bundle hash verified" tar xzf "$TARBALL" -C "$BIN_DIR" bin/dico-provider 2>/dev/null \ || tar xzf "$TARBALL" -C "$BIN_DIR" 2>/dev/null rm -f "$TARBALL" local BIN_PATH="$BIN_DIR/dico-provider" if [ ! -x "$BIN_PATH" ]; then BIN_PATH="$(find "$BIN_DIR" -name dico-provider -type f 2>/dev/null | head -1)" fi if [ -z "$BIN_PATH" ] || [ ! -f "$BIN_PATH" ]; then echo " ✗ dico-provider binary not found in bundle." return 1 fi chmod +x "$BIN_PATH" if [ -n "$BINARY_HASH" ]; then local ACTUAL_BIN="" ACTUAL_BIN=$(sha256sum "$BIN_PATH" 2>/dev/null | cut -d' ' -f1 \ || shasum -a 256 "$BIN_PATH" 2>/dev/null | cut -d' ' -f1 || echo "") if [ "$ACTUAL_BIN" != "$BINARY_HASH" ]; then echo " ✗ Binary hash mismatch." return 1 fi echo " ✓ Binary hash verified" fi # Symlink to a stable location. Prefer ~/.local/bin (per-user, no sudo); # also create a `dicompute` alias so the branded command works. mkdir -p "$HOME/.local/bin" 2>/dev/null || true local LINK_DIR="$HOME/.local/bin" ln -sf "$BIN_PATH" "$LINK_DIR/dico-provider" 2>/dev/null \ || { ln -sf "$BIN_PATH" /usr/local/bin/dico-provider 2>/dev/null && LINK_DIR="/usr/local/bin"; } || true ln -sf "$BIN_PATH" "$LINK_DIR/dicompute" 2>/dev/null || true # Ensure the link dir is on PATH — the #1 "command not found after install" # cause on Ubuntu is ~/.local/bin not being on PATH (issue #198). Append to # the user's shell rc (idempotent) and export for the current shell. ensure_on_path "$LINK_DIR" echo "→ [4/4] Running setup (installs systemd user unit) ..." DICO_WS="$WS" DICO_MODEL="${DICO_MODEL:-qwen2.5-3b-instruct}" \ "$BIN_PATH" setup || { echo " ✗ setup failed — run manually:" echo " DICO_WS=$WS $BIN_PATH setup" return 1 } echo "" echo "✓ dico-provider installed at $BIN_PATH" echo " (linked as 'dico-provider' and 'dicompute' in $LINK_DIR)" echo "" echo " Hardware detection and earnings estimate will be shown when the" echo " provider starts (dico-provider serve). The connector detects your" echo " GPU/CPU, selects the best-fit model, and prints a summary." echo "" echo " Open a NEW terminal, or run: export PATH=\"$LINK_DIR:\$PATH\"" echo "" echo " Next steps:" echo " dico-provider login # link to your DICOMPUTE account" echo " dico-provider start # start the provider now" echo " loginctl enable-linger \$USER # auto-start at boot (no login needed)" echo " dico-provider doctor # check readiness" return 0 } echo "╔══════════════════════════════════════════════╗" echo "║ DICOMPUTE — Private AI on Verified Macs ║" echo "╚══════════════════════════════════════════════╝" echo "" # ─── Pre-flight checks ─────────────────────────────────────── # Apple Silicon Macs run the verified MLX provider below. Any other host # (Intel Mac, Linux) joins via the portable llama.cpp tier instead. if [ "$(uname)" != "Darwin" ] || [ "$(uname -m)" != "arm64" ]; then dico_portable_install exit $? fi CHIP=$(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo "Apple Silicon") MEM=$(sysctl -n hw.memsize 2>/dev/null | awk '{printf "%.0f", $1/1073741824}') SERIAL=$(ioreg -c IOPlatformExpertDevice -d 2 | awk -F'"' '/IOPlatformSerialNumber/{print $4}') MACOS=$(sw_vers -productVersion 2>/dev/null || echo "?") echo " $CHIP · ${MEM}GB · macOS $MACOS" echo "" # ─── Step 1: Fetch latest release ──────────────────────────── echo "→ [1/5] Fetching latest release from $COORD_URL ..." RELEASE_JSON=$(fetch_release "$COORD_URL/v1/releases/latest?platform=macos-arm64" "macos-arm64") || exit 1 if [ -z "$RELEASE_JSON" ]; then echo " ✗ Empty release response from coordinator." exit 1 fi # Extract JSON string fields with sed — no python3 needed (no Xcode CLT prompt). json_val() { echo "$1" | sed -n "s/.*\"$2\":\"\([^\"]*\)\".*/\1/p"; } BUNDLE_URL=$(json_val "$RELEASE_JSON" url) BUNDLE_HASH=$(json_val "$RELEASE_JSON" bundle_hash) BINARY_HASH=$(json_val "$RELEASE_JSON" binary_hash) METALLIB_HASH=$(json_val "$RELEASE_JSON" metallib_hash) VERSION=$(json_val "$RELEASE_JSON" version) BACKEND=$(json_val "$RELEASE_JSON" backend) if [ -z "$BUNDLE_URL" ] || [ -z "$BUNDLE_HASH" ] || [ -z "$VERSION" ]; then echo " ✗ Coordinator response missing required fields (url / bundle_hash / version)." echo " Raw response: $RELEASE_JSON" exit 1 fi echo " Version: $VERSION" echo " Backend: ${BACKEND:-mlx-swift}" echo " Signed by: Developer ID Application: Eigen Labs, Inc." echo "" # ─── Step 2: Download + verify bundle ──────────────────────── echo "→ [2/5] Downloading DICOMPUTE v${VERSION}..." mkdir -p "$INSTALL_DIR" "$BIN_DIR" TARBALL="/tmp/darkbloom-bundle.tar.gz" curl -f#L "$BUNDLE_URL" -o "$TARBALL" ACTUAL_HASH=$(shasum -a 256 "$TARBALL" | cut -d' ' -f1) if [ "$ACTUAL_HASH" != "$BUNDLE_HASH" ]; then echo "" echo " ✗ Bundle hash mismatch — refusing to install possibly-tampered binary." echo " Expected: $BUNDLE_HASH" echo " Got: $ACTUAL_HASH" rm -f "$TARBALL" exit 1 fi echo " Bundle hash verified ✓" echo " Staging and verifying the complete app before touching the live install ..." if ! install_bundle_atomically "$TARBALL" "$INSTALL_DIR" "$BINARY_HASH" "$METALLIB_HASH"; then rm -f "$TARBALL" echo " Existing installation was left unchanged." exit 1 fi ln -sfn "$BIN_DIR/darkbloom-enclave" "$BIN_DIR/eigeninference-enclave" 2>/dev/null || true # DICOMPUTE command aliases. The signed binary is still named `darkbloom` (the # bundle rename ships with a future signed release); these let operators use the # `dicompute …` commands the console documents. ln -sfn "$BIN_DIR/darkbloom" "$BIN_DIR/dicompute" 2>/dev/null || true ln -sfn "$BIN_DIR/darkbloom-enclave" "$BIN_DIR/dicompute-enclave" 2>/dev/null || true rm -f "$TARBALL" echo " Strict signature, runtime resources, and atomic swap verified ✓" # Make available in PATH. Try /usr/local/bin symlink, fall back to shell rc. if ln -sf "$BIN_DIR/darkbloom" /usr/local/bin/darkbloom 2>/dev/null; then ln -sf "$BIN_DIR/darkbloom" /usr/local/bin/dicompute 2>/dev/null || true fi RC="$HOME/.zshrc" if [ -f "$HOME/.bashrc" ] && [ ! -f "$HOME/.zshrc" ]; then RC="$HOME/.bashrc" fi if ! grep -q "\.darkbloom/bin" "$RC" 2>/dev/null; then sed -i '' '/\.dginf\/bin/d; /\.eigeninference\/bin/d; /alias eigeninf/d; /alias dginf/d; /# EigenInference/d; /# Darkbloom$/d' "$RC" 2>/dev/null || true cat >> "$RC" << 'SHELL' # Darkbloom export PATH="$HOME/.darkbloom/bin:$PATH" SHELL fi export PATH="$BIN_DIR:$PATH" # Source rc so commands work in this shell. Disable -eu around it: rc files # may use unbound vars or shell-specific builtins that fail under bash strict. set +eu source "$RC" 2>/dev/null || true set -eu echo " Binaries installed ✓" echo " Shortcut: dicompute" # ─── Migrate from old installs ─────────────────────────────── # Migration chain: ~/.dginf → ~/.eigeninference → ~/.darkbloom for OLD_DIR in "$HOME/.dginf" "$HOME/.eigeninference"; do if [ -d "$OLD_DIR" ] && [ ! -L "$OLD_DIR" ]; then echo "" echo " Migrating from $OLD_DIR..." for f in enclave_key.data wallet_key auth_token; do [ -f "$OLD_DIR/$f" ] && cp -n "$OLD_DIR/$f" "$INSTALL_DIR/$f" 2>/dev/null || true done # Symlink old path so stragglers still work, then drop the old python/ # subtree -- the Swift release no longer needs it. ln -sfn "$INSTALL_DIR" "$OLD_DIR" 2>/dev/null || true echo " Migration complete ✓" fi done # ─── Step 3: Secure Enclave identity ───────────────────────── echo "" echo "→ [3/5] Provisioning Secure Enclave identity..." if "$BIN_DIR/darkbloom-enclave" info >/dev/null 2>&1; then echo " Secure Enclave ✓ (P-256 key generated)" else echo " Secure Enclave ⚠ (not available on this hardware; provider will run with reduced trust)" fi # ─── Step 4: Enrollment + device attestation ───────────────── echo "" echo "→ [4/5] Enrollment + device attestation..." ALREADY_ENROLLED=false if profiles status -type enrollment 2>&1 | grep -q "MDM enrollment: Yes"; then ALREADY_ENROLLED=true fi if [ "$ALREADY_ENROLLED" = true ]; then echo " Already enrolled ✓" elif [ -n "$SERIAL" ]; then echo " Requesting enrollment profile from coordinator..." PROFILE_PATH="/tmp/Darkbloom-Enroll-${SERIAL}.mobileconfig" rm -f "$PROFILE_PATH" 2>/dev/null if curl -fsSL -X POST "$COORD_URL/v1/enroll" \ -H "Content-Type: application/json" \ -d "{\"serial_number\": \"$SERIAL\"}" \ -o "$PROFILE_PATH" 2>/dev/null; then echo "" echo " ┌──────────────────────────────────────────────────┐" echo " │ ACTION REQUIRED: Install the enrollment profile │" echo " │ │" echo " │ This profile lets the coordinator verify: │" echo " │ • SIP, Secure Boot, system integrity │" echo " │ • Your Secure Enclave is genuine Apple silicon │" echo " │ • Device identity signed by Apple's Root CA │" echo " │ │" echo " │ DICOMPUTE CANNOT erase, lock, or control │" echo " │ your Mac. Remove anytime in System Settings. │" echo " └──────────────────────────────────────────────────┘" echo "" open "$PROFILE_PATH" sleep 1 open "x-apple.systempreferences:com.apple.Profiles-Settings.extension" echo " System Settings opened — click Install and enter your password." if [ "$INTERACTIVE" = true ]; then echo "" read -p " Press Enter once you have installed the profile..." || true else echo " After installing, the provider will verify on first start." sleep 3 fi if profiles status -type enrollment 2>&1 | grep -q "MDM enrollment: Yes"; then echo " Enrollment verified ✓" else echo " Enrollment pending ⚠ (complete it in System Settings, or run: darkbloom enroll)" fi else echo " Enrollment ⚠ (coordinator unreachable — enroll later with: dicompute enroll)" fi else echo " Enrollment ⚠ (could not read serial number — enroll later with: dicompute enroll)" fi # ─── Step 5: Optional starter model ────────────────────────── echo "" echo "→ [5/5] Inference model..." CATALOG_JSON=$(curl -fsSL "$COORD_URL/v1/models/catalog?type=text" 2>/dev/null || echo "") if [ -n "$CATALOG_JSON" ]; then if [ "$INTERACTIVE" = true ]; then echo "" echo " Available text models:" echo "$CATALOG_JSON" \ | tr ',' '\n' \ | sed -n 's/.*"id":"\([^"]*\)".*"display_name":"\([^"]*\)".*"size_gb":\([0-9.]*\).*"min_ram_gb":\([0-9]*\).*/ • \2 ~\3 GB (≥\4 GB RAM) [\1]/p' \ | head -20 echo "" echo " Download a model with:" echo " dicompute models download " else echo " Run interactively to pick a starter model:" echo " curl -fsSL $COORD_URL/install.sh | bash -s" echo " Or list the catalog with: dicompute models catalog" fi else echo " Could not fetch model catalog (continuing anyway)." fi # ─── Done ──────────────────────────────────────────────────── echo "" echo "╔══════════════════════════════════════════════╗" echo "║ Install complete ║" echo "╚══════════════════════════════════════════════╝" echo "" echo " Next steps:" echo " dicompute doctor # verify the system is ready" echo " dicompute models catalog # browse available models" echo " dicompute models download " echo " dicompute login # link this Mac to your account" echo " dicompute start # serve inference (interactive picker)" echo ""