#!/bin/sh set -eu # Teev runner installer. Published as https://teev.ai/install (a static file in # the landing-page site) and run by customers with the one command shown in the # console's Runner section: # # curl -fsSL https://teev.ai/install -o /tmp/teev-install.sh && \ # TEEV_BOOTSTRAP_TOKEN= sh /tmp/teev-install.sh # # The script preflights Docker and Node, downloads a pinned, checksummed runner # release (no clone required), unpacks it to ~/teev-runner and starts the # runner. Re-running it upgrades an enrolled runner in place: .teev state, model # credentials and packaged work are never touched. The bootstrap token is # single-use and is ignored when a runner is already enrolled. # # Overridable for testing: TEEV_DOWNLOAD_BASE, TEEV_INSTALL_DIR, # TEEV_RUNNER_VERSION, TEEV_RUNNER_SCOPE, TEEV_CONTROL_PLANE. TEEV_DEFAULT_RUNNER_VERSION="0.1.29" TEEV_DOWNLOAD_BASE="${TEEV_DOWNLOAD_BASE:-https://teev.ai/releases}" TEEV_INSTALL_DIR="${TEEV_INSTALL_DIR:-$HOME/teev-runner}" TEEV_RUNNER_VERSION="${TEEV_RUNNER_VERSION:-$TEEV_DEFAULT_RUNNER_VERSION}" TEEV_RUNNER_SCOPE="${TEEV_RUNNER_SCOPE:-personal}" TEEV_CONTROL_PLANE="${TEEV_CONTROL_PLANE:-https://api.teev.ai}" TEEV_INSTALL_STARTED_AT="${TEEV_INSTALL_STARTED_AT:-$(date +%s)}" export TEEV_INSTALL_STARTED_AT if [ -z "${TEEV_EXECUTION_BACKEND:-}" ]; then if [ "$(uname -s)" = Darwin ]; then TEEV_EXECUTION_BACKEND=sbx; else TEEV_EXECUTION_BACKEND=docker; fi fi export TEEV_EXECUTION_BACKEND if [ "$TEEV_EXECUTION_BACKEND" = sbx ]; then TEEV_CONTROL_PLANE=$(printf '%s' "$TEEV_CONTROL_PLANE" | sed 's|://host\.docker\.internal|://127.0.0.1|') fi # Tell the console this token is being redeemed, before Docker is even # preflighted below — the runtime build that follows can run for several # minutes with nothing else to report. Best-effort and silent: a token that # is missing, already consumed, or a network that drops this ping must never # hold up or fail the install, and there is nothing useful to do with the # response anyway. if [ -n "${TEEV_BOOTSTRAP_TOKEN:-}" ]; then curl -fsS -m 5 -X POST "$TEEV_CONTROL_PLANE/api/agent/bootstrap/ping" \ -H 'Content-Type: application/json' \ -d "{\"bootstrapToken\":\"$TEEV_BOOTSTRAP_TOKEN\"}" >/dev/null 2>&1 || true fi if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then TEEV_CYAN='\033[36m' TEEV_GREEN='\033[32m' TEEV_DIM='\033[2m' TEEV_RESET='\033[0m' else TEEV_CYAN= TEEV_GREEN= TEEV_DIM= TEEV_RESET= fi timer_row_completed() { [ -n "${TEEV_VERSION_TIMER_OFFSET_FILE:-}" ] && [ -f "$TEEV_VERSION_TIMER_OFFSET_FILE" ] || return offset=$(cat "$TEEV_VERSION_TIMER_OFFSET_FILE" 2>/dev/null || true) case "$offset" in ''|*[!0-9]*) return ;; esac printf '%s\n' $((offset + 1)) >"$TEEV_VERSION_TIMER_OFFSET_FILE" } info() { printf '%s\n' "$*"; timer_row_completed; } clear_progress() { [ ! -t 1 ] || printf '\r\033[2K' >&2; } warn() { clear_progress; printf ' ! %s\n' "$*" >&2; timer_row_completed; } error() { clear_progress; printf ' ✕ %s\n' "$*" >&2; timer_row_completed; exit 1; } elapsed() { now=$(date +%s) seconds=$((now - TEEV_INSTALL_STARTED_AT)) [ "$seconds" -ge 0 ] || seconds=0 hours=$((seconds / 3600)) minutes=$(((seconds % 3600) / 60)) seconds=$((seconds % 60)) if [ "$hours" -gt 0 ]; then printf '%d:%02d:%02d' "$hours" "$minutes" "$seconds" else printf '%02d:%02d' "$minutes" "$seconds" fi } version_value() { if [ "$TEEV_RUNNER_VERSION" = "$TEEV_DEFAULT_RUNNER_VERSION" ]; then printf 'v%s' "$TEEV_RUNNER_VERSION" else printf 'v%s (pinned; default v%s)' "$TEEV_RUNNER_VERSION" "$TEEV_DEFAULT_RUNNER_VERSION" fi } refresh_version_timer() { [ -t 1 ] && [ -f "${TEEV_VERSION_TIMER_OFFSET_FILE:-}" ] || return offset=$(cat "$TEEV_VERSION_TIMER_OFFSET_FILE" 2>/dev/null || true) case "$offset" in ''|0|*[!0-9]*) return ;; esac # Save the active cursor, repaint the header above the accumulated status # rows, then return without disturbing the in-progress spinner. printf '\033[s\033[%sA\r\033[2K %bVersion%b %s (elapsed %s)\n\033[u' \ "$offset" "$TEEV_DIM" "$TEEV_RESET" "$(version_value)" "$(elapsed)" } start_version_timer() { [ -t 1 ] || return TEEV_VERSION_TIMER_OFFSET_FILE=${TMPDIR:-/tmp}/teev-installer-lines-$$ export TEEV_VERSION_TIMER_OFFSET_FILE # One blank row and the active progress row initially sit below the header. printf '2\n' >"$TEEV_VERSION_TIMER_OFFSET_FILE" installer_pid=$$ ( while kill -0 "$installer_pid" 2>/dev/null; do sleep 1 refresh_version_timer done ) & VERSION_TIMER_PID=$! } stop_version_timer() { [ -n "${VERSION_TIMER_PID:-}" ] || return kill "$VERSION_TIMER_PID" 2>/dev/null || true wait "$VERSION_TIMER_PID" 2>/dev/null || true VERSION_TIMER_PID= refresh_version_timer rm -f "$TEEV_VERSION_TIMER_OFFSET_FILE" TEEV_VERSION_TIMER_OFFSET_FILE= } step() { shift [ ! -t 1 ] || printf '\r\033[2K' if [ -t 1 ]; then printf ' %b◓%b %s' "$TEEV_CYAN" "$TEEV_RESET" "$*" else printf ' ◇ %s\n' "$*" fi } done_step() { shift clear_progress printf ' %b✓%b %s\n' "$TEEV_GREEN" "$TEEV_RESET" "$*" timer_row_completed } configure_model_access() { if [ ! -t 0 ] || [ ! -t 1 ]; then return; fi while :; do printf '\n %b→%b Add Model Provider Now? [Y/n] ' "$TEEV_CYAN" "$TEEV_RESET" IFS= read -r answer || answer=n case "$answer" in ''|y|Y|yes|YES|Yes) if [ "$TEEV_EXECUTION_BACKEND" = sbx ]; then TEEV_DATA_DIR="$TEEV_INSTALL_DIR/.teev" TEEV_EXECUTION_BACKEND=sbx "$TEEV_INSTALL_DIR/teev" init else docker compose exec teev-runner teev init fi return ;; n|N|no|NO|No) return ;; *) info " Enter y or n." ;; esac done } wordmark() { if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then printf '\033[36m'; fi cat <<'EOF' ███████╗ █████╗ █████╗ ██╗ ██╗ ╚═██╔══╝██╔══██╗██╔══██╗██║ ██║ ██║ ███████║███████║██║ ██║ ██║ ██╔════╝██╔════╝╚██╗ ██╔╝ ██║ ╚██████╗╚██████╗ ╚████╔╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═══╝ EOF if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then printf '\033[0m'; fi printf '\n' printf ' %bVersion%b %s (elapsed %s)\n' \ "$TEEV_DIM" "$TEEV_RESET" "$(version_value)" "$(elapsed)" printf '\n' } # A dim label against its value, aligned without visual chrome. field() { if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then printf ' \033[2m%-9s\033[0m %s\n' "$1" "$2" else printf ' %-9s %s\n' "$1" "$2" fi } checksum_of() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' else sha256sum "$1" | awk '{print $1}' fi } case "$TEEV_RUNNER_SCOPE" in personal|tenant) ;; *) error "TEEV_RUNNER_SCOPE must be personal or tenant" ;; esac # The supported command downloads this script before executing it, leaving # stdin attached to the terminal for setup prompts. Do not redirect stdin here: # when sh is still reading a piped script, doing so makes it read the remaining # program from /dev/tty and wait silently for the user to type shell source. # --- Preflight ---------------------------------------------------------- wordmark start_version_timer trap stop_version_timer EXIT step 5 "Checking This Device" case "$(uname -s)" in Darwin|Linux) ;; *) error "unsupported OS: $(uname -s). Teev runs on macOS and Linux; on Windows use WSL2." ;; esac case "$(uname -m)" in x86_64|arm64) ;; *) error "unsupported architecture: $(uname -m)" ;; esac command -v curl >/dev/null 2>&1 || error "curl is required to run the installer" command -v tar >/dev/null 2>&1 || error "tar is required to run the installer" command -v docker >/dev/null 2>&1 \ || error "Docker is not installed. Install Docker Desktop (macOS) or Docker Engine (Linux) first." docker info >/dev/null 2>&1 || error "Docker is installed but not running. Start it and run this command again." if [ "$TEEV_EXECUTION_BACKEND" = docker ]; then docker compose version >/dev/null 2>&1 \ || error "Docker Compose v2 is required (the docker compose command must exist)" elif [ "$TEEV_EXECUTION_BACKEND" = sbx ]; then [ "$(uname -s)" = Darwin ] \ || error "The host-native sbx runner currently supports macOS; use TEEV_EXECUTION_BACKEND=docker on this host." if ! command -v sbx >/dev/null 2>&1; then command -v brew >/dev/null 2>&1 \ || error "Docker Sandboxes is not installed. Install sbx from https://docs.docker.com/ai/sandboxes/install/." info "Installing Docker Sandboxes with Homebrew..." brew trust docker/tap brew install docker/tap/sbx fi sbx version >/dev/null 2>&1 || error "Docker Sandboxes is installed but unavailable. Run 'sbx diagnose'." # The human-formatted list can wait indefinitely for terminal rendering in # sbx 0.39.0 when its output is redirected. JSON mode is non-interactive and # exercises the same authenticated daemon request. if ! sbx ls --json >/dev/null 2>&1; then info "Signing in to Docker Sandboxes..." sbx login || error "Docker Sandboxes sign-in did not complete" fi if ! sbx policy ls >/dev/null 2>&1; then info "Initializing Docker Sandboxes with Teev's deny-by-default network policy..." sbx policy init deny-all fi POLICY_CHECK=$(sbx policy check network registry.npmjs.org 2>&1 || true) printf '%s' "$POLICY_CHECK" | grep -qi denied \ || error "Docker Sandboxes currently allows development sites globally. Teev requires the Locked Down preset: run 'sbx policy reset', then rerun this installer and choose deny-all." else error "TEEV_EXECUTION_BACKEND must be docker or sbx" fi if command -v node >/dev/null 2>&1; then NODE_MAJOR=$(node -e 'process.stdout.write(String(process.versions.node.split(".")[0]))') if ! [ "$NODE_MAJOR" -ge 22 ] 2>/dev/null; then error "Node.js 22 or newer is required (found $(node --version 2>/dev/null || printf unknown))" fi else error "Node.js 22 or newer is required. Install it from https://nodejs.org and run this command again." fi FREE_KB=$(df -Pk "$(dirname "$TEEV_INSTALL_DIR")" 2>/dev/null | awk 'NR == 2 {print $4}') if [ -n "${FREE_KB:-}" ] && ! [ "$FREE_KB" -ge 20000000 ] 2>/dev/null; then warn "less than 20 GB free on the disk holding $TEEV_INSTALL_DIR" fi done_step 15 "Device Ready" # --- Existing state ----------------------------------------------------- ENROLLED=no if [ -s "$TEEV_INSTALL_DIR/.teev/runner-token.json" ] \ && grep -q runnerToken "$TEEV_INSTALL_DIR/.teev/runner-token.json" 2>/dev/null; then ENROLLED=yes fi OLD_VERSION=$(cat "$TEEV_INSTALL_DIR/.teev-install-version" 2>/dev/null || printf 'none') TOKEN="${TEEV_BOOTSTRAP_TOKEN:-}" if [ "$ENROLLED" = no ] && [ -z "$TOKEN" ]; then if [ -t 0 ] && [ -t 1 ]; then printf 'Paste the one-time runner token from the console (Setup > Runner): ' stty -echo IFS= read -r TOKEN stty echo printf '\n' [ -n "$TOKEN" ] || error "no token entered" else error "TEEV_BOOTSTRAP_TOKEN is not set and there is no terminal to prompt for one" fi elif [ "$ENROLLED" = yes ] && [ -n "$TOKEN" ]; then INSTALL_NOTE="A runner is already enrolled in $TEEV_INSTALL_DIR; the new token was not needed." unset TEEV_BOOTSTRAP_TOKEN fi # --- Download and unpack ------------------------------------------------ TARBALL="runner-v${TEEV_RUNNER_VERSION}.tar.gz" TMP_DIR="$TEEV_INSTALL_DIR/.teev-installer-tmp" mkdir -p "$TEEV_INSTALL_DIR" "$TMP_DIR" step 20 "Downloading Runner v$TEEV_RUNNER_VERSION" curl -fsSL --retry 3 "$TEEV_DOWNLOAD_BASE/$TARBALL" -o "$TMP_DIR/$TARBALL" \ || error "could not download $TEEV_DOWNLOAD_BASE/$TARBALL" curl -fsSL --retry 3 "$TEEV_DOWNLOAD_BASE/$TARBALL.sha256" -o "$TMP_DIR/$TARBALL.sha256" \ || error "could not download $TEEV_DOWNLOAD_BASE/$TARBALL.sha256" EXPECTED=$(awk '{print $1}' "$TMP_DIR/$TARBALL.sha256") ACTUAL=$(checksum_of "$TMP_DIR/$TARBALL") [ -n "$EXPECTED" ] && [ "$EXPECTED" = "$ACTUAL" ] \ || error "checksum mismatch for $TARBALL (expected $EXPECTED, got $ACTUAL)" done_step 30 "Runner Downloaded & Verified" tar -xzf "$TMP_DIR/$TARBALL" -C "$TEEV_INSTALL_DIR" printf '%s\n' "$TEEV_RUNNER_VERSION" > "$TEEV_INSTALL_DIR/.teev-install-version" rm -rf "$TMP_DIR" cd "$TEEV_INSTALL_DIR" # --- Enroll or upgrade -------------------------------------------------- if [ "$ENROLLED" = no ]; then export TEEV_BOOTSTRAP_TOKEN="$TOKEN" export TEEV_CONTROL_PLANE sh ./deploy/local/setup.sh "$TEEV_RUNNER_SCOPE" no elif [ "$OLD_VERSION" = "$TEEV_RUNNER_VERSION" ]; then if [ "$TEEV_EXECUTION_BACKEND" = sbx ]; then sh ./deploy/local/setup.sh "$TEEV_RUNNER_SCOPE" no else docker compose up -d fi INSTALL_NOTE="Runner v$TEEV_RUNNER_VERSION was already enrolled and current." else if [ "$TEEV_EXECUTION_BACKEND" = sbx ]; then sh ./deploy/local/setup.sh "$TEEV_RUNNER_SCOPE" no else docker compose up -d --build --force-recreate waited=0 while ! docker compose exec -T teev-runner sh -c 'grep -q runnerToken /var/lib/teev/runner-token.json' >/dev/null 2>&1; do if [ -z "$(docker compose ps --status running -q teev-runner)" ]; then printf '\n' >&2 docker compose logs --no-color --tail=100 teev-runner >&2 || true error "runner stopped before the upgrade completed" fi if [ "$waited" -ge 1800 ]; then printf '\n' >&2 docker compose logs --no-color --tail=100 teev-runner >&2 || true error "runner did not become healthy within 30 minutes" fi if [ $((waited % 10)) -eq 0 ]; then printf '.'; fi sleep 2 waited=$((waited + 2)) done printf ' done\n' timer_row_completed fi INSTALL_NOTE="Upgraded from v$OLD_VERSION to v$TEEV_RUNNER_VERSION. Runner state and credentials were preserved." fi stop_version_timer done_step 100 "Teev Runner Online" [ -z "${INSTALL_NOTE:-}" ] || printf ' %b%s%b\n' "$TEEV_DIM" "$INSTALL_NOTE" "$TEEV_RESET" configure_model_access printf '\n' info " Return to the console to continue." if [ "$TEEV_EXECUTION_BACKEND" = sbx ]; then info " Add or change model access later: $TEEV_INSTALL_DIR/teev init" else info " Add or change model access later: docker compose exec teev-runner teev init" fi