#!/usr/bin/env sh set -e # --- Utilities --- have_cmd() { command -v "$1" >/dev/null 2>&1; } info() { printf "\033[1;34m ==>\033[0m %s\n" "$1"; } ok() { printf "\033[1;32m ==>\033[0m %s\n" "$1"; } warn() { printf "\033[1;33m [WARN]\033[0m %s\n" "$1"; } fail() { printf "\033[1;31m [ERROR]\033[0m %s\n" "$1"; exit 1; } # --- Platform detection --- OS="$(uname -s)" ARCH="$(uname -m)" case "$OS" in Linux) OS="linux" ;; Darwin) OS="darwin" ;; *) fail "Unsupported operating system: $OS Policate currently supports Linux (x86_64, ARM64) and macOS (x86_64, ARM64). For native Windows, download the checksum-verified x64 EXE from https://policate.marcllort.com/releases/latest." ;; esac case "$ARCH" in x86_64|amd64) ARCH="x64" ;; aarch64|arm64) ARCH="arm64" ;; *) fail "Unsupported architecture: $ARCH Policate currently supports x86_64 and ARM64." ;; esac ARTIFACT="policate-$OS-$ARCH" info "Detected platform: $OS-$ARCH" # --- Determine install directory --- if [ -d "$HOME/.local/bin" ] && echo ":$PATH:" | grep -q ":$HOME/.local/bin:"; then INSTALL_DIR="$HOME/.local/bin" elif command -v sudo >/dev/null 2>&1 && [ -w "/usr/local/bin" ]; then INSTALL_DIR="/usr/local/bin" elif [ -d "$HOME/.local/bin" ]; then INSTALL_DIR="$HOME/.local/bin" warn "$HOME/.local/bin is not in your PATH. Add it to your shell config: export PATH="$HOME/.local/bin:$PATH"" else INSTALL_DIR="$HOME/.local/bin" mkdir -p "$INSTALL_DIR" warn "$HOME/.local/bin is not in your PATH. Add it to your shell config: export PATH="$HOME/.local/bin:$PATH"" fi BINARY_PATH="$INSTALL_DIR/policate" if [ -f "$BINARY_PATH" ]; then info "Existing policate binary found at $BINARY_PATH, will overwrite" fi # --- Detect authenticated download prerequisites --- if have_cmd curl; then download() { case "$1" in https://policate.marcllort.com/*) ;; *) fail "Refusing untrusted release URL: $1" ;; esac curl --fail --silent --show-error --proto '=https' --proto-redir '=https' --max-redirs 0 "$1" --output "$2" } elif have_cmd wget; then download() { case "$1" in https://policate.marcllort.com/*) ;; *) fail "Refusing untrusted release URL: $1" ;; esac wget --quiet --https-only --max-redirect=0 --output-document="$2" "$1" } else fail "Neither curl nor wget found. Install one of them and try again." fi have_cmd openssl || fail "OpenSSL is required to authenticate Policate releases. Install it and try again." # --- Resolve first-party public release artifacts --- RELEASE_BASE="https://policate.marcllort.com/releases/latest" MANIFEST_URL="$RELEASE_BASE/release-manifest.json" SIGNATURE_URL="$RELEASE_BASE/release-manifest.sig" info "Using Policate public release mirror: $RELEASE_BASE" # --- Authenticate release metadata before choosing a version or binary --- TMP_DIR=$(mktemp -d) trap 'rm -rf "$TMP_DIR"' EXIT TMP_BINARY="$TMP_DIR/$ARTIFACT" TMP_MANIFEST="$TMP_DIR/release-manifest.json" TMP_SIGNATURE="$TMP_DIR/release-manifest.sig" PUBLIC_KEY="$TMP_DIR/policate-cli-release-v1.pem" cat > "$PUBLIC_KEY" <<'POLICATE_RELEASE_PUBLIC_KEY' -----BEGIN PUBLIC KEY----- MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAnjvyAYYLERg2ZHMgZTN6 hXJ1tpBeF5e4SlzSrouaCpXVlpV/AHN0WJ94Nr75+pzB4PcBjFDqFclTT9bDGTf8 5F/rI3O6GSd1eZBVhhpo3C8tRlcUCVHzZiivBKQ+jRpIkLnXzKsLrzOEfL6xMgUB Vn/DzenvZGuoU1AXiRd77PIXzjLghDHCTp00KxUPKMCCt5p/dEH0Vdosbzll+KkX 0SvoAnhzAuNkoKqPjMs+Ft9k5iVwWzruOnCuDyRvyZC7ghCtnoyOGLSDP1FZSmSb JRtPv4GuDoDCbrcspVuBY5eFdzLAwfwArbva/v4Jc/g27ea1Ny0rgPT3kHbG9sDt A1l2U1NwmrXWyVjrq+miWT/cxePNTrlbrBW+kpk5gzuinYLl2AhomjZ+nP/2a4n8 fsnwtFyIPF+odt5yk6oWgn34aUacZF9BgvL6qkpTEmPqJtNRk25C2xYPSsdTxQoe f9ERBWTzQyfa3g3badiCKGEe2rAub1G8ZTY1xYZBFxmBAgMBAAE= -----END PUBLIC KEY----- POLICATE_RELEASE_PUBLIC_KEY info "Authenticating signed release manifest..." download "$MANIFEST_URL" "$TMP_MANIFEST" || fail "Could not fetch release manifest: $MANIFEST_URL" download "$SIGNATURE_URL" "$TMP_SIGNATURE" || fail "Could not fetch release signature: $SIGNATURE_URL" [ -s "$TMP_MANIFEST" ] && [ -s "$TMP_SIGNATURE" ] || fail "Release manifest or signature is empty." [ "$(wc -c < "$TMP_MANIFEST" | tr -d ' ')" -le 131072 ] || fail "Release manifest exceeds the size limit." openssl dgst -sha256 -verify "$PUBLIC_KEY" -signature "$TMP_SIGNATURE" "$TMP_MANIFEST" >/dev/null 2>&1 || fail "Release manifest signature verification failed. Refusing to install." VERSION=$(awk 'index($0, "version") { gsub(/[",]/, "", $2); print $2; exit }' "$TMP_MANIFEST") TAG=$(awk 'index($0, "tag") { gsub(/[",]/, "", $2); print $2; exit }' "$TMP_MANIFEST") SCHEMA=$(awk 'index($0, "schema") { gsub(/[",]/, "", $2); print $2; exit }' "$TMP_MANIFEST") PRODUCT=$(awk 'index($0, "product") { gsub(/[",]/, "", $2); print $2; exit }' "$TMP_MANIFEST") SOURCE_REPOSITORY=$(awk 'index($0, "repository") { gsub(/[",]/, "", $2); print $2; exit }' "$TMP_MANIFEST") SOURCE_SHA=$(awk 'index($0, "sha") && !index($0, "sha256") { gsub(/[",]/, "", $2); print $2; exit }' "$TMP_MANIFEST") UPSTREAM_REF=$(awk 'index($0, "ref") { gsub(/[",]/, "", $2); print $2; exit }' "$TMP_MANIFEST") UPSTREAM_COMMIT=$(awk 'index($0, "commit") { gsub(/[",]/, "", $2); print $2; exit }' "$TMP_MANIFEST") case "$VERSION" in ''|*[!0-9A-Za-z.-]*) fail "Signed release manifest has an invalid version." ;; esac [ "$TAG" = "cli/v$VERSION" ] || fail "Signed release tag is not bound to version $VERSION." [ "$SCHEMA" = "policate-cli-release/v1" ] && [ "$PRODUCT" = "policate-cli" ] || fail "Unknown signed release manifest schema or product." [ "$SOURCE_REPOSITORY" = "https://github.com/marcllort/policate" ] || fail "Signed release manifest has invalid source provenance." printf '%s' "$SOURCE_SHA" | awk 'length($0) >= 40 && length($0) <= 64 && $0 !~ /[^0-9a-f]/ { valid=1 } END { exit !valid }' || fail "Signed release manifest has invalid source provenance." case "$UPSTREAM_REF" in v[0-9]*.[0-9]*.[0-9]*) ;; *) fail "Signed release manifest has invalid upstream provenance." ;; esac printf '%s' "$UPSTREAM_COMMIT" | awk 'length($0) >= 40 && length($0) <= 64 && $0 !~ /[^0-9a-f]/ { valid=1 } END { exit !valid }' || fail "Signed release manifest has invalid upstream provenance." EXPECTED_SHA256=$(awk -v artifact="$ARTIFACT" ' index($0, artifact) { found=1; next } found && index($0, "size") { gsub(/[^0-9]/, "", $2); size=$2; next } found && index($0, "sha256") { gsub(/[^0-9a-f]/, "", $2); print size " " $2; exit } ' "$TMP_MANIFEST") EXPECTED_SIZE=$(printf '%s' "$EXPECTED_SHA256" | awk '{print $1}') EXPECTED_SHA256=$(printf '%s' "$EXPECTED_SHA256" | awk '{print $2}') case "$EXPECTED_SIZE" in ''|*[!0-9]*) fail "Signed release manifest has no valid size for $ARTIFACT." ;; esac printf '%s' "$EXPECTED_SHA256" | awk 'length($0) == 64 && $0 !~ /[^0-9a-f]/ { valid=1 } END { exit !valid }' || fail "Signed release manifest has no valid SHA-256 for $ARTIFACT." IMMUTABLE_BASE="https://policate.marcllort.com/releases/$VERSION" IMMUTABLE_MANIFEST="$TMP_DIR/immutable-release-manifest.json" IMMUTABLE_SIGNATURE="$TMP_DIR/immutable-release-manifest.sig" download "$IMMUTABLE_BASE/release-manifest.json" "$IMMUTABLE_MANIFEST" || fail "Could not fetch immutable release manifest." download "$IMMUTABLE_BASE/release-manifest.sig" "$IMMUTABLE_SIGNATURE" || fail "Could not fetch immutable release signature." cmp "$TMP_MANIFEST" "$IMMUTABLE_MANIFEST" >/dev/null 2>&1 || fail "Latest and immutable release manifests do not match." cmp "$TMP_SIGNATURE" "$IMMUTABLE_SIGNATURE" >/dev/null 2>&1 || fail "Latest and immutable release signatures do not match." openssl dgst -sha256 -verify "$PUBLIC_KEY" -signature "$IMMUTABLE_SIGNATURE" "$IMMUTABLE_MANIFEST" >/dev/null 2>&1 || fail "Immutable release manifest signature verification failed." info "Downloading authenticated $ARTIFACT for Policate $VERSION..." download "$IMMUTABLE_BASE/$ARTIFACT" "$TMP_BINARY" || fail "Download failed: $IMMUTABLE_BASE/$ARTIFACT" ACTUAL_SIZE=$(wc -c < "$TMP_BINARY" | tr -d ' ') [ "$ACTUAL_SIZE" = "$EXPECTED_SIZE" ] || fail "Release size mismatch. Expected $EXPECTED_SIZE bytes, got $ACTUAL_SIZE." if have_cmd sha256sum; then COMPUTED_SHA256=$(sha256sum "$TMP_BINARY" | awk '{print $1}') elif have_cmd shasum; then COMPUTED_SHA256=$(shasum -a 256 "$TMP_BINARY" | awk '{print $1}') else fail "No SHA256 tool found. Install sha256sum or shasum before installing Policate." fi if [ "$COMPUTED_SHA256" = "$EXPECTED_SHA256" ]; then ok "SHA256 checksum verified" else fail "SHA256 checksum mismatch! Expected: $EXPECTED_SHA256 Got: $COMPUTED_SHA256 The downloaded binary may be corrupted or tampered with. Aborting installation." fi chmod +x "$TMP_BINARY" || fail "Failed to make the verified download executable" REPORTED_VERSION=$("$TMP_BINARY" --version 2>/dev/null) || fail "The verified binary failed its startup check." case "$REPORTED_VERSION" in "policate $VERSION (runtime "*")") ;; *) fail "The verified binary reports '$REPORTED_VERSION', expected Policate $VERSION." ;; esac # --- Install binary --- mkdir -p "$INSTALL_DIR" STAGED_BINARY="$INSTALL_DIR/.policate-install-$$" trap 'rm -rf "$TMP_DIR"; rm -f "$STAGED_BINARY"' EXIT cp "$TMP_BINARY" "$STAGED_BINARY" || fail "Failed to stage binary in $INSTALL_DIR" chmod +x "$STAGED_BINARY" || fail "Failed to make the staged binary executable" "$STAGED_BINARY" --version >/dev/null 2>&1 || fail "The staged binary failed its startup check." # Rename is atomic within one directory. Existing agent processes keep their # original executable inode while new invocations receive the verified release. mv -f "$STAGED_BINARY" "$BINARY_PATH" || fail "Failed to activate binary at $BINARY_PATH" ok "Policate $VERSION installed successfully at $BINARY_PATH" # --- Verify --- INSTALLED_VERSION=$("$BINARY_PATH" --version 2>&1) ok "Installed version: $INSTALLED_VERSION" echo "" info "Next, sign in to connect this machine to your Policate workspace: policate login " info "After login, send prompts through the gateway: policate 'your prompt here' " info "Need help? Visit https://policate.marcllort.com/docs"