#!/bin/sh # Orquesta Agent Installer — standalone binary, no Node.js required set -e PURPLE="\033[35m" GREEN="\033[32m" RED="\033[31m" CYAN="\033[36m" DIM="\033[2m" BOLD="\033[1m" RESET="\033[0m" BASE_URL="https://getorquesta.com" # Parse arguments TOKEN="" CLI_MODE="" SERVICE_MODE=false shift_next=false for arg in "$@"; do case "$arg" in --token=*) TOKEN="${arg#*=}" ;; --service=*) TOKEN="${arg#*=}"; SERVICE_MODE=true ;; --mode=*) CLI_MODE="${arg#*=}" ;; --cli-preference=*) CLI_MODE="${arg#*=}" ;; --token) shift_next=token ;; --service) shift_next=service; SERVICE_MODE=true ;; --mode|--cli-preference) shift_next=mode ;; *) if [ "$shift_next" = "token" ]; then TOKEN="$arg"; shift_next=false elif [ "$shift_next" = "service" ]; then TOKEN="$arg"; shift_next=false elif [ "$shift_next" = "mode" ]; then CLI_MODE="$arg"; shift_next=false; fi ;; esac done # Build optional mode flag to append to agent invocations MODE_FLAG="" if [ -n "$CLI_MODE" ]; then MODE_FLAG="--mode $CLI_MODE" fi echo "" echo "${BOLD}${PURPLE} getorquesta${RESET}${BOLD}.com${RESET} — Agent Installer" echo "" # ── Detect platform ─────────────────────────────────── OS="$(uname -s)" ARCH="$(uname -m)" case "$OS" in Linux*) PLATFORM="linux" ;; Darwin*) PLATFORM="macos" ;; *) echo " ${RED}Unsupported OS: $OS${RESET}"; exit 1 ;; esac case "$ARCH" in x86_64|amd64) ARCH="x64" ;; aarch64|arm64) ARCH="arm64" ;; *) echo " ${RED}Unsupported architecture: $ARCH${RESET}"; exit 1 ;; esac BINARY_NAME="orquesta-agent-${PLATFORM}-${ARCH}" BINARY_URL="${BASE_URL}/agent/${BINARY_NAME}" FALLBACK_URL="${BASE_URL}/agent/orquesta-agent.mjs" MANIFEST_URL="${BASE_URL}/agent/manifest.json" echo " ${DIM}Platform: ${PLATFORM}/${ARCH}${RESET}" # ── Install location ────────────────────────────────── # macOS: never reach for sudo. A `curl | sh` install has no usable stdin, so a # sudo password prompt either blocks forever or aborts a non-interactive install # entirely. The LaunchAgent PATH already includes $HOME/.local/bin, so the daemon # finds the binaries there regardless. (/usr/local/bin is still used when it's # already writable — e.g. Homebrew on Intel — so no sudo is needed.) INSTALL_DIR="/usr/local/bin" if [ -w "$INSTALL_DIR" ]; then SUDO="" elif [ "$PLATFORM" = "macos" ]; then INSTALL_DIR="$HOME/.local/bin" mkdir -p "$INSTALL_DIR" SUDO="" elif command -v sudo >/dev/null 2>&1; then SUDO="sudo" else INSTALL_DIR="$HOME/.local/bin" mkdir -p "$INSTALL_DIR" SUDO="" fi # If the chosen dir isn't on PATH, the daemon still works (LaunchAgent/systemd set # their own PATH), but an interactive `orquesta` call wouldn't resolve. Hint once. case ":$PATH:" in *":$INSTALL_DIR:"*) ;; *) PATH_HINT="$INSTALL_DIR" ;; esac TARGET="$INSTALL_DIR/orquesta-agent" # ── Download ────────────────────────────────────────── download() { # Retry transient failures (up to 3 attempts). The agent + cli binaries are # ~100MB each, so a mid-download network blip is plausible; without retry the # cli download silently degrades to "install Node" (the agent still installs). _dl_i=0 while [ "$_dl_i" -lt 3 ]; do if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$1" "$2" && return 0 elif command -v wget >/dev/null 2>&1; then wget -q -O "$1" "$2" && return 0 else echo " ${RED}Neither curl nor wget found.${RESET}"; exit 1 fi _dl_i=$((_dl_i+1)) [ "$_dl_i" -lt 3 ] && sleep 2 done return 1 } # Try standalone binary first (no Node.js needed) echo " Downloading standalone binary..." TMP_FILE="$(mktemp)" if download "$TMP_FILE" "$BINARY_URL" 2>/dev/null; then $SUDO mv "$TMP_FILE" "$TARGET" $SUDO chmod +x "$TARGET" MODE="binary" else # Fallback: download JS bundle (needs Node.js) echo " ${DIM}Binary not available for ${PLATFORM}/${ARCH}, trying JS bundle...${RESET}" if ! command -v node >/dev/null 2>&1; then echo " ${RED}Node.js required for JS fallback. Install Node.js 20+:${RESET}" echo " curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash -" echo " sudo apt-get install -y nodejs" rm -f "$TMP_FILE" exit 1 fi BUNDLE_PATH="$INSTALL_DIR/orquesta-agent-bundle.mjs" download "$TMP_FILE" "$FALLBACK_URL" $SUDO mv "$TMP_FILE" "$BUNDLE_PATH" # Create wrapper script $SUDO tee "$TARGET" > /dev/null << 'WRAPPER' #!/bin/sh exec node "$(dirname "$(readlink -f "$0" 2>/dev/null || echo "$0")")/orquesta-agent-bundle.mjs" "$@" WRAPPER $SUDO chmod +x "$TARGET" MODE="js-bundle" fi rm -f "$TMP_FILE" # ── Verify manifest signature (pinned Orquesta key) ─── # Closed-source provenance: the manifest is signed with Orquesta's private key # (which never touches this file origin). We refuse to install if the signature # doesn't verify against the PINNED public key below — so even a fully # compromised getorquesta.com cannot serve a tampered binary + matching SHA. # Skips gracefully when no signature is published yet (older deploys) or when # openssl is unavailable (the SHA check below still ties the binary to the # manifest; the signature is what makes the manifest itself trustworthy). ORQUESTA_MANIFEST_PUBKEY="-----BEGIN PUBLIC KEY----- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE0O/tk6EKno1DvaS+xYJ7OQhtktiH Tco+T/yYEIlboVIuOqWnKhInYurUN4wTeuUel26qAs47GkfUDTHcnIktAg== -----END PUBLIC KEY-----" SIG_URL="${BASE_URL}/agent/manifest.json.sig" if command -v openssl >/dev/null 2>&1; then SIG_TMP="$(mktemp)" if download "$SIG_TMP" "$SIG_URL" 2>/dev/null && [ -s "$SIG_TMP" ]; then MANIFEST_TMP="$(mktemp)"; SIG_BIN="$(mktemp)"; PUB_TMP="$(mktemp)" download "$MANIFEST_TMP" "$MANIFEST_URL" 2>/dev/null # Read via stdin, not a positional file: macOS base64 rejects a positional # input arg (exit 64), which under `set -e` aborted the WHOLE install on macOS # before the cli/node-pty steps. stdin works on both BSD (macOS) and GNU. ( base64 -d < "$SIG_TMP" > "$SIG_BIN" 2>/dev/null ) || ( base64 -D < "$SIG_TMP" > "$SIG_BIN" 2>/dev/null ) printf '%s\n' "$ORQUESTA_MANIFEST_PUBKEY" > "$PUB_TMP" if openssl dgst -sha256 -verify "$PUB_TMP" -signature "$SIG_BIN" "$MANIFEST_TMP" >/dev/null 2>&1; then echo " ${GREEN}Manifest signature verified (Orquesta key)${RESET}" else echo " ${RED}Manifest signature INVALID — refusing to install.${RESET}" echo " ${RED}The download may have been tampered with. Aborting.${RESET}" $SUDO rm -f "$TARGET" rm -f "$SIG_TMP" "$MANIFEST_TMP" "$SIG_BIN" "$PUB_TMP" exit 1 fi rm -f "$MANIFEST_TMP" "$SIG_BIN" "$PUB_TMP" else echo " ${DIM}No manifest signature published — skipping signature check${RESET}" fi rm -f "$SIG_TMP" else echo " ${DIM}openssl not found — skipping manifest signature check${RESET}" fi # ── Verify checksum ─────────────────────────────────── INSTALLED_VERSION="$("$TARGET" --version 2>/dev/null | tr -d '"' || echo "unknown")" # Download manifest and verify SHA-256 (sha256sum on Linux, shasum on macOS) EXPECTED_HASH="" SHA_CMD="" if command -v sha256sum >/dev/null 2>&1; then SHA_CMD="sha256sum" elif command -v shasum >/dev/null 2>&1; then SHA_CMD="shasum -a 256" fi if [ -n "$SHA_CMD" ]; then MANIFEST="$(download /dev/stdout "$MANIFEST_URL" 2>/dev/null || echo "")" if [ -n "$MANIFEST" ]; then if [ "$MODE" = "binary" ]; then # Extract sha256 from the per-platform block: "-": { ..., "sha256": "..." } EXPECTED_HASH="$(echo "$MANIFEST" | sed -n '/"'"${PLATFORM}-${ARCH}"'"/,/}/p' | grep -o '"sha256": *"[a-f0-9]*"' | head -1 | grep -o '[a-f0-9]\{64\}')" ACTUAL_HASH="$($SHA_CMD "$TARGET" | cut -d' ' -f1)" else EXPECTED_HASH="$(echo "$MANIFEST" | grep -A1 '"bundle"' | grep -o '"sha256": *"[a-f0-9]*"' | grep -o '[a-f0-9]\{64\}')" BUNDLE_FILE="$INSTALL_DIR/orquesta-agent-bundle.mjs" ACTUAL_HASH="$($SHA_CMD "$BUNDLE_FILE" | cut -d' ' -f1)" fi if [ -n "$EXPECTED_HASH" ] && [ "$ACTUAL_HASH" = "$EXPECTED_HASH" ]; then echo " ${GREEN}SHA-256 verified${RESET}" elif [ -n "$EXPECTED_HASH" ]; then echo " ${RED}SHA-256 mismatch! Expected: $EXPECTED_HASH${RESET}" echo " ${RED} Got: $ACTUAL_HASH${RESET}" echo " ${RED}The binary may have been tampered with.${RESET}" exit 1 fi fi fi echo "" if [ "$MODE" = "binary" ]; then echo " ${GREEN}${BOLD}orquesta-agent@$INSTALLED_VERSION installed (standalone binary)${RESET}" else echo " ${GREEN}${BOLD}orquesta-agent@$INSTALLED_VERSION installed (JS bundle + Node.js)${RESET}" fi echo " ${DIM}Location: $TARGET${RESET}" echo " ${DIM}Verify: curl -s $MANIFEST_URL | jq .${RESET}" # ── Install orquesta-cli (the coding engine the agent spawns) ───────── # A no-Node standalone agent can't npm-install the cli, and without a cli on # PATH every dispatched prompt fails ("No coding CLI installed"). Download the # matching standalone cli binary and place it as `orquesta` so the agent finds # it on PATH. Verified against its own signed manifest (same pinned key). echo "" echo " Installing orquesta-cli (coding engine)..." CLI_BINARY_NAME="orquesta-cli-${PLATFORM}-${ARCH}" CLI_BINARY_URL="${BASE_URL}/cli/${CLI_BINARY_NAME}" CLI_MANIFEST_URL="${BASE_URL}/cli/manifest.json" CLI_SIG_URL="${BASE_URL}/cli/manifest.json.sig" CLI_TARGET="$INSTALL_DIR/orquesta" CLI_TMP="$(mktemp)" if download "$CLI_TMP" "$CLI_BINARY_URL" 2>/dev/null && [ -s "$CLI_TMP" ]; then # Verify the cli manifest signature against the SAME pinned Orquesta key. if command -v openssl >/dev/null 2>&1; then CLI_SIG_TMP="$(mktemp)" if download "$CLI_SIG_TMP" "$CLI_SIG_URL" 2>/dev/null && [ -s "$CLI_SIG_TMP" ]; then CLI_MAN_TMP="$(mktemp)"; CLI_SIG_BIN="$(mktemp)"; CLI_PUB_TMP="$(mktemp)" download "$CLI_MAN_TMP" "$CLI_MANIFEST_URL" 2>/dev/null ( base64 -d < "$CLI_SIG_TMP" > "$CLI_SIG_BIN" 2>/dev/null ) || ( base64 -D < "$CLI_SIG_TMP" > "$CLI_SIG_BIN" 2>/dev/null ) printf '%s\n' "$ORQUESTA_MANIFEST_PUBKEY" > "$CLI_PUB_TMP" if ! openssl dgst -sha256 -verify "$CLI_PUB_TMP" -signature "$CLI_SIG_BIN" "$CLI_MAN_TMP" >/dev/null 2>&1; then echo " ${RED}orquesta-cli manifest signature INVALID — skipping cli install.${RESET}" CLI_TMP="" fi rm -f "$CLI_MAN_TMP" "$CLI_SIG_BIN" "$CLI_PUB_TMP" fi rm -f "$CLI_SIG_TMP" fi # Verify SHA-256 against the cli manifest. if [ -n "$CLI_TMP" ] && [ -n "$SHA_CMD" ]; then CLI_MANIFEST="$(download /dev/stdout "$CLI_MANIFEST_URL" 2>/dev/null || echo "")" CLI_EXPECTED="$(echo "$CLI_MANIFEST" | sed -n '/"'"${PLATFORM}-${ARCH}"'"/,/}/p' | grep -o '"sha256": *"[a-f0-9]*"' | head -1 | grep -o '[a-f0-9]\{64\}')" CLI_ACTUAL="$($SHA_CMD "$CLI_TMP" | cut -d' ' -f1)" if [ -n "$CLI_EXPECTED" ] && [ "$CLI_ACTUAL" != "$CLI_EXPECTED" ]; then echo " ${RED}orquesta-cli SHA-256 mismatch — skipping cli install.${RESET}" CLI_TMP="" fi fi if [ -n "$CLI_TMP" ]; then $SUDO mv "$CLI_TMP" "$CLI_TARGET" $SUDO chmod +x "$CLI_TARGET" CLI_VER="$("$CLI_TARGET" --version 2>/dev/null | tr -d '"' || echo "unknown")" echo " ${GREEN}orquesta-cli@$CLI_VER installed → $CLI_TARGET${RESET}" fi else echo " ${DIM}No standalone orquesta-cli for ${PLATFORM}/${ARCH} — install Node + 'npm i -g orquesta-cli' to run prompts.${RESET}" fi rm -f "$CLI_TMP" 2>/dev/null || true # ── Provision the node-pty binding (interactive sessions) ───────────── # Interactive sessions need @homebridge/node-pty-prebuilt-multiarch, which the # standalone binary can't embed. We place the prebuilt binding NOW, at install # time, into the agent's user cache — so a box with NO Node/npm still gets working # sessions. (Without this, the agent's only recourse is a runtime `npm install`, # which a no-Node box — exactly what these binaries are for — can't run.) The # tarball is verified against the SAME signed agent manifest. # # PER-PLATFORM tarballs (each built on its own OS — a prebuild fetched on one OS # never includes another's binding): linux uses the bundled-prebuilds tarball # (nodePty, linux-x64/arm64 via node-gyp-build); macOS x64 uses a darwin-built # tarball carrying build/Release/pty.node (nodePtyMacosX64). Windows is handled by # install.ps1 (nodePtyWin32). macOS arm64 and anything else fall back to the agent's # runtime npm self-heal (needs Node) — we never ship an unverified binding, since a # wrong/missing one would "load" then fail at spawn, masking the self-heal. The key # is matched EXACTLY (closing quote in the pattern) so "nodePty" can't match # nodePtyWin32 / nodePtyMacosX64. NPT_KEY="" if [ "$PLATFORM" = "linux" ]; then NPT_KEY="nodePty" elif [ "$PLATFORM" = "macos" ] && [ "$ARCH" = "x64" ]; then NPT_KEY="nodePtyMacosX64" fi if [ -n "$NPT_KEY" ] && command -v tar >/dev/null 2>&1; then echo "" echo " Installing interactive-session support (node-pty)..." NPT_DEST="$HOME/.orquesta-agent/native/node_modules/@homebridge" NPT_MANIFEST="$(download /dev/stdout "$MANIFEST_URL" 2>/dev/null || echo "")" NPT_FILE="$(echo "$NPT_MANIFEST" | sed -n "/\"$NPT_KEY\"/,/}/p" | grep -o '"file": *"[^"]*"' | head -1 | sed 's/.*"file": *"//;s/"//')" NPT_EXPECTED="$(echo "$NPT_MANIFEST" | sed -n "/\"$NPT_KEY\"/,/}/p" | grep -o '"sha256": *"[a-f0-9]*"' | head -1 | grep -o '[a-f0-9]\{64\}')" if [ -n "$NPT_FILE" ]; then NPT_TMP="$(mktemp)" if download "$NPT_TMP" "${BASE_URL}/agent/${NPT_FILE}" 2>/dev/null && [ -s "$NPT_TMP" ]; then NPT_OK=1 if [ -n "$SHA_CMD" ] && [ -n "$NPT_EXPECTED" ]; then NPT_ACTUAL="$($SHA_CMD "$NPT_TMP" | cut -d' ' -f1)" if [ "$NPT_ACTUAL" != "$NPT_EXPECTED" ]; then echo " ${RED}node-pty SHA-256 mismatch — skipping (sessions will self-heal if Node is present).${RESET}" NPT_OK=0 fi fi if [ "$NPT_OK" = "1" ]; then mkdir -p "$NPT_DEST" if tar -xzf "$NPT_TMP" -C "$NPT_DEST" 2>/dev/null; then echo " ${GREEN}Interactive sessions ready (node-pty installed)${RESET}" else echo " ${DIM}Could not extract node-pty — sessions will self-heal on first use.${RESET}" fi fi else echo " ${DIM}node-pty binding not available — sessions will self-heal on first use.${RESET}" fi rm -f "$NPT_TMP" fi fi # ── macOS LaunchAgent installer ─────────────────────── # A foreground daemon launched from `curl | sh` dies the moment the terminal # closes, and the installer had no launchd path (systemd is Linux-only), so macOS # users had no persistent agent — the "downloaded but didn't stay running" gap. # This writes a per-user LaunchAgent that runs the agent in the background, # (re)starts it on login, and relaunches it if it crashes (KeepAlive). install_launchd_agent() { LABEL="com.orquesta.agent" PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" LOG_DIR="$HOME/Library/Logs" mkdir -p "$HOME/Library/LaunchAgents" "$LOG_DIR" # Optional --mode flag, rendered as plist elements. MODE_PLIST="" if [ -n "$CLI_MODE" ]; then MODE_PLIST=" --mode $CLI_MODE " fi cat > "$PLIST" << PLISTEOF Label $LABEL ProgramArguments $TARGET --token $TOKEN --daemon ${MODE_PLIST} RunAtLoad KeepAlive WorkingDirectory $WORK_DIR StandardOutPath $LOG_DIR/orquesta-agent.log StandardErrorPath $LOG_DIR/orquesta-agent.log EnvironmentVariables PATH /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:$HOME/.local/bin ProcessType Background PLISTEOF # Load + start. Prefer modern bootstrap; fall back to legacy load -w on older # macOS. bootout first so a re-run cleanly replaces a prior install. UID_NUM="$(id -u)" launchctl bootout "gui/$UID_NUM/$LABEL" 2>/dev/null || true launchctl bootstrap "gui/$UID_NUM" "$PLIST" 2>/dev/null || launchctl load -w "$PLIST" 2>/dev/null || true launchctl kickstart -k "gui/$UID_NUM/$LABEL" 2>/dev/null || true sleep 1 if launchctl list 2>/dev/null | grep -q "$LABEL"; then echo " ${GREEN}${BOLD}LaunchAgent installed — agent running in the background.${RESET}" echo " ${DIM}Starts automatically on login; relaunches if it crashes.${RESET}" else echo " ${RED}LaunchAgent may have failed to start. Check the log below.${RESET}" fi echo "" echo " ${BOLD}Manage:${RESET}" echo " tail -f $LOG_DIR/orquesta-agent.log" echo " launchctl kickstart -k gui/$UID_NUM/$LABEL # restart" echo " launchctl bootout gui/$UID_NUM/$LABEL # stop & remove" echo "" } # If we installed somewhere off PATH, tell the user how to reach the CLI directly. # The background daemon is unaffected (it sets its own PATH), but an interactive # `orquesta`/`orquesta-agent` call would otherwise "command not found". if [ -n "$PATH_HINT" ]; then echo "" echo " ${DIM}$PATH_HINT is not on your PATH. To run the CLI directly, add:${RESET}" echo " export PATH=\"$PATH_HINT:\$PATH\"" fi # ── Persistent service / auto-connect ───────────────── # macOS: always install a LaunchAgent (a foreground daemon from `curl | sh` would # die when the terminal closes). Linux: --service installs a systemd unit; a plain # --token runs a foreground daemon (unchanged). if [ -n "$TOKEN" ]; then WORK_DIR="$(pwd)" if [ "$PLATFORM" = "macos" ]; then echo "" echo " Setting up LaunchAgent..." install_launchd_agent exit 0 fi if [ "$SERVICE_MODE" = true ]; then echo "" echo " Setting up systemd service..." CURRENT_USER="$(whoami)" $SUDO tee /etc/systemd/system/orquesta-agent.service > /dev/null << SVCEOF [Unit] Description=Orquesta Agent After=network.target [Service] Type=simple User=$CURRENT_USER WorkingDirectory=$WORK_DIR ExecStart=$TARGET --token $TOKEN --daemon $MODE_FLAG Restart=always RestartSec=10 Environment=NODE_ENV=production Environment=PATH=/usr/local/bin:/usr/bin:/bin:$HOME/.local/bin [Install] WantedBy=multi-user.target SVCEOF $SUDO systemctl daemon-reload $SUDO systemctl enable orquesta-agent $SUDO systemctl start orquesta-agent sleep 2 if systemctl is-active --quiet orquesta-agent 2>/dev/null; then echo " ${GREEN}Service started and enabled on boot.${RESET}" else echo " ${RED}Service may have failed. Check: sudo journalctl -u orquesta-agent${RESET}" fi echo "" echo " ${BOLD}Manage:${RESET}" echo " sudo systemctl status orquesta-agent" echo " sudo systemctl restart orquesta-agent" echo " sudo journalctl -u orquesta-agent -f" echo "" exit 0 fi # Linux foreground auto-connect (--token without --service) echo "" echo " Starting agent..." echo "" if [ -n "$CLI_MODE" ]; then exec "$TARGET" --token "$TOKEN" --daemon --mode "$CLI_MODE" else exec "$TARGET" --token "$TOKEN" --daemon fi fi # ── No token — show instructions ────────────────────── echo "" echo " ${BOLD}Quick start:${RESET}" echo " orquesta-agent --token " echo "" echo " ${BOLD}Run as daemon (auto-restart):${RESET}" echo " orquesta-agent --token --daemon" echo "" echo " ${BOLD}Install as system service:${RESET}" echo " curl -fsSL $BASE_URL/install | sh -s -- --service " echo "" echo " ${BOLD}Web UI:${RESET}" echo " orquesta-agent ui" echo "" echo " ${DIM}Get your token at $BASE_URL${RESET}" echo ""