#!/bin/sh # KnowMyAgents — one-line installer (macOS / Linux) # # curl -fsSL https://knowmyagents.com/install.sh | sh # # Non-interactive (CI / scripted): preset the two values as env vars — # curl -fsSL https://knowmyagents.com/install.sh | KMA_TENANT= KMA_TOKEN= sh # # It checks Docker, downloads the kit, writes .env (generating the local # secrets for you), and starts KMA. The two things it CANNOT do for you — # minting a token and approving the machine — are your two security locks. set -eu SITE="${KMA_SITE:-https://knowmyagents.com}" CONSOLE="${CONSOLE_URL:-https://console.knowmyagents.com}" VERSION="${KMA_VERSION:-0.9.25}" UPSTREAM="${UPSTREAM_URL:-}" # empty: no door at install time; add doors from the console say() { printf '%s\n' "$*"; } die() { printf 'x %s\n' "$*" >&2; exit 1; } # A 32-char lowercase-hex secret, portable across sh implementations. gen_secret() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex 16; return; fi LC_ALL=C tr -dc 'a-f0-9' < /dev/urandom 2>/dev/null | dd bs=1 count=32 2>/dev/null } # Read a value: prefer the given env var, else prompt from the real terminal # (/dev/tty works even when this script is piped from curl). ask() { # ask VARNAME "Prompt" _v=$(eval "printf '%s' \"\${$1:-}\"") if [ -n "$_v" ]; then printf '%s' "$_v"; return; fi printf '%s' "$2" > /dev/tty read _in < /dev/tty printf '%s' "$_in" } say "KnowMyAgents installer" say "----------------------" # 1. Docker must be installed AND running. command -v docker >/dev/null 2>&1 || die "Docker isn't installed. Get Docker Desktop: https://www.docker.com/products/docker-desktop/ then re-run this." docker info >/dev/null 2>&1 || die "Docker is installed but not running. Start Docker Desktop, then re-run this." docker compose version >/dev/null 2>&1 || die "Your Docker is too old (no 'docker compose'). Update Docker Desktop." # Where to install. KMA_DIR wins (scripted/non-interactive); otherwise ASK, # defaulting to the stable home the update/backup scripts and docs assume. Enter # accepts the default. (It never uses the current directory: a piped # `curl | sh` has no meaningful "current folder", so it must be chosen.) DIR=$(ask KMA_DIR "Install folder [$HOME/kma]: ") DIR="${DIR:-$HOME/kma}" # 1a. Already installed here? Then this is an UPDATE, not a fresh install: keep # the machine's secrets, registration, audit log and passports, and bring the # kit to the latest version. Set KMA_FRESH=1 to force a fresh install instead # (you will get a new machine identity and must approve it in the console). if [ -f "$DIR/.env" ] && [ -f "$DIR/docker-compose.yml" ] && [ "${KMA_FRESH:-0}" != "1" ]; then _cur=$(sed -n 's/^KMA_VERSION=//p' "$DIR/.env" | head -n1) say "KMA is already installed in $DIR (version ${_cur:-unknown})." say "Updating it to $VERSION — your secrets, registration, audit log and passports are kept." say "(To start over instead, run with KMA_FRESH=1 — that makes a NEW machine identity.)" say "" curl -fsSL "$SITE/kit/update.sh" -o "$DIR/update.sh" || die "could not download the update script" chmod +x "$DIR/update.sh" cd "$DIR" && KMA_VERSION="$VERSION" exec sh ./update.sh fi # 1b. Ports. KMA publishes four host ports; anything already listening on one # would fail `docker compose up` minutes from now with an opaque Docker # error. Probe them NOW: a taken port silently moves to the next free one, # recorded in .env and announced. Preset KMA__PORT to choose your own. ASSIGNED="" # ports this very run has already handed to another KMA service port_free() { case " $ASSIGNED " in *" $1 "*) return 1 ;; esac # A restarting/starting container claims its port inside dockerd before any # socket listens — the OS probe alone misses it. Ask docker too. if docker ps --format '{{.Ports}}' 2>/dev/null | grep -q ":$1->"; then return 1; fi if command -v nc >/dev/null 2>&1; then ! nc -z 127.0.0.1 "$1" >/dev/null 2>&1 elif command -v lsof >/dev/null 2>&1; then ! lsof -nP -iTCP:"$1" -sTCP:LISTEN >/dev/null 2>&1 else return 0; fi # no probe tool: skip the check rather than block the install } port_holder() { # best-effort: the docker container publishing this host port, if any docker ps --format '{{.Names}}\t{{.Ports}}' 2>/dev/null | awk -v p=":$1->" 'index($0,p){print $1; exit}' } pick_port() { _p="$1" while ! port_free "$_p"; do _p=$((_p+1)); done printf '%s' "$_p" } PORT_MOVED=0 for _spec in "VIEWER 8085" "LOG 8084" "PASSPORT 8081" "GATEWAY 8129"; do _name=${_spec% *}; _def=${_spec#* } _want=$(eval "printf '%s' \"\${KMA_${_name}_PORT:-$_def}\"") _got=$(pick_port "$_want") eval "KMA_${_name}_PORT=$_got" ASSIGNED="$ASSIGNED $_got" if [ "$_got" != "$_want" ]; then _who=$(port_holder "$_want") say " ! port $_want is taken${_who:+ (docker container: $_who)} — $_name moves to $_got" PORT_MOVED=1 fi done [ "$PORT_MOVED" = 1 ] && say " (moved ports are saved in .env; preset KMA__PORT to pick your own)" # 2. The ONE value you need: a registration token. The tenant is looked up from # it (read-only — the lookup does NOT consume the token; the kit redeems it). say "" say "You need ONE thing from your console ($CONSOLE -> Settings ->" say "Registration tokens -> Mint): a registration token. It's shown once." say "" TOKEN=$(ask KMA_TOKEN "Registration token: ") [ -n "$TOKEN" ] || die "A registration token is required." TENANT="${KMA_TENANT:-}" if [ -z "$TENANT" ]; then say "Looking up your organization from the token..." TI=$(curl -fsS -X POST "$CONSOLE/v1/registrations/token-info" \ -H "Content-Type: application/json" -d "{\"token\":\"$TOKEN\"}" 2>/dev/null || true) TENANT=$(printf '%s' "$TI" | sed -n 's/.*"tenant_id"[ :]*"\([^"]*\)".*/\1/p') fi [ -n "$TENANT" ] || die "Couldn't read your organization from that token — it may be wrong, expired, or already used. Mint a fresh one in the console." MACHINE="${MACHINE_NAME:-$(hostname 2>/dev/null || echo my-machine)}" # 3. Fetch the kit into $DIR (compose + the scripts it mounts). mkdir -p "$DIR/passport" for f in docker-compose.yml Caddyfile enroll.sh mint.sh update.sh backup.sh restore.sh INSTALL.md; do curl -fsSL "$SITE/kit/$f" -o "$DIR/$f" || die "Couldn't download kit/$f from $SITE." done chmod +x "$DIR"/*.sh 2>/dev/null || true # the operator scripts you'll run directly # 4. Write .env — your values, plus generated local secrets that never leave here. cat > "$DIR/.env" </dev/null 2>&1; then sha256sum "$1" | awk '{print $1}' elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' else openssl dgst -sha256 -r "$1" | awk '{print $1}'; fi } install_sensors() { OS=$(uname -s | tr '[:upper:]' '[:lower:]') case "$(uname -m)" in x86_64|amd64) ARCH=amd64 ;; arm64|aarch64) ARCH=arm64 ;; *) say " ! unsupported CPU ($(uname -m)) — skipping sensors"; return 1 ;; esac BIN="$DIR/bin/kma" # The release binary IS the sensor distributable (the artifacts are embedded # in it) — download once, checksum-verified, reused if already current. if [ ! -x "$BIN" ] || ! "$BIN" --version 2>/dev/null | grep -q "$VERSION"; then ART="kma_${VERSION}_${OS}_${ARCH}.tar.gz" say " downloading kma $VERSION ($OS/$ARCH)..." TMP=$(mktemp -d) curl -fsSL "$RELEASES/$ART" -o "$TMP/$ART" \ || { say " ! couldn't download $ART from $RELEASES"; rm -rf "$TMP"; return 1; } curl -fsSL "$RELEASES/checksums.txt" -o "$TMP/checksums.txt" \ || { say " ! couldn't download checksums.txt from $RELEASES"; rm -rf "$TMP"; return 1; } WANT=$(awk -v f="$ART" '$2==f{print $1}' "$TMP/checksums.txt") GOT=$(file_sha256 "$TMP/$ART") if [ -z "$WANT" ] || [ "$WANT" != "$GOT" ]; then say " ! checksum mismatch for $ART — refusing to install it"; rm -rf "$TMP"; return 1 fi mkdir -p "$DIR/bin" ( cd "$TMP" && tar -xzf "$ART" kma && install -m 0755 kma "$BIN" ) \ || { say " ! couldn't extract kma from $ART"; rm -rf "$TMP"; return 1; } rm -rf "$TMP" fi # Mint a SCOPED enrollment token with the admin secret generated above — # the sensors hold only the kmaet_ token, never the admin token itself. LADMIN=$(sed -n 's/^LOG_ADMIN_TOKEN=//p' "$DIR/.env") curl -fsS http://127.0.0.1:$KMA_LOG_PORT/healthz >/dev/null 2>&1 \ || { say " ! kma-log isn't reachable on 127.0.0.1:8084 (older kit compose without the loopback port?)"; return 1; } ETOK=$(curl -fsS -X POST http://127.0.0.1:$KMA_LOG_PORT/v1/enrollment-tokens \ -H "Authorization: Bearer $LADMIN" -H "X-KMA-Tenant: $TENANT" \ -H "Content-Type: application/json" \ -d "{\"label\":\"installer $MACHINE\"}" 2>/dev/null \ | sed -n 's/.*"token"[ :]*"\(kmaet_[^"]*\)".*/\1/p') [ -n "$ETOK" ] || { say " ! couldn't mint an enrollment token"; return 1; } # One-token setup; per-item consent stays inside the CLI. Interactive runs # answer from the real terminal; KMA_SENSORS=1 means auto-yes everywhere. case "${KMA_SENSORS:-}" in 1|yes|true) "$BIN" sensor init --token "$ETOK" --hub http://127.0.0.1:$KMA_LOG_PORT --yes < /dev/null ;; *) "$BIN" sensor init --token "$ETOK" --hub http://127.0.0.1:$KMA_LOG_PORT < /dev/tty ;; esac || { say " ! kma sensor init reported an error"; return 1; } } SENSORS_LINE="" if sensors_wanted; then say "" say "Setting up the client sensors..." if install_sensors; then SENSORS_LINE="Sensors: installed — the viewer's Status page shows them announce themselves. Manage with: $DIR/bin/kma sensor status" else SENSORS_LINE="Sensors: not set up — finish any time: download kma from $RELEASES then run: kma sensor init --hub http://127.0.0.1:$KMA_LOG_PORT" fi fi # 7. The one human step left. say "" say "OK KMA is running. Two steps to finish:" say " 1) Approve this machine: $CONSOLE -> Settings -> Registrations (it shows as pending)." say " 2) Start a program through KMA: $CONSOLE -> Programs -> Add a program" say " (nothing is added to the program; its places appear on Doors by themselves)" say "" say "Your local viewer: http://localhost:$KMA_VIEWER_PORT | Fleet view: $CONSOLE" [ -n "$SENSORS_LINE" ] && say "$SENSORS_LINE" say "Installed in: $DIR (docker compose down to stop; -v to erase)"