#!/usr/bin/env sh set -eu INSTALL_DIR="${PLATFORM_AGENT_DIR:-/opt/platform-agent}" SERVICE_NAME="${PLATFORM_AGENT_SERVICE:-platform-agent}" REPO_URL="${PLATFORM_AGENT_REPO_URL:-}" RUN_USER="${PLATFORM_AGENT_USER:-$(id -un)}" INSTALL_TAILSCALE="${INSTALL_TAILSCALE:-false}" INSTALL_OPENSSH_SERVER="${INSTALL_OPENSSH_SERVER:-true}" INSTALL_GIT="${INSTALL_GIT:-true}" TAILSCALE_AUTH_KEY="${TAILSCALE_AUTH_KEY:-}" TAILSCALE_LOGIN_SERVER="${TAILSCALE_LOGIN_SERVER:-}" TAILSCALE_HOSTNAME="${TAILSCALE_HOSTNAME:-}" TAILSCALE_EXTRA_ARGS="${TAILSCALE_EXTRA_ARGS:-}" PLATFORM_PROVISION_URL="${PLATFORM_PROVISION_URL:-https://portal-api.autonomystudio.net/api/provision}" PLATFORM_ACCOUNT="${PLATFORM_ACCOUNT:-}" PLATFORM_REGISTRATION_CODE="${PLATFORM_REGISTRATION_CODE:-}" WORKSPACE_ROOT="${WORKSPACE_ROOT:-~/autonomy-studio/workspaces}" export WORKSPACE_ROOT ROBOT_STACK="${ROBOT_STACK:-autoware}" ROS_DOMAIN_ID="${ROS_DOMAIN_ID:-0}" CAMERA_ENABLED="${CAMERA_ENABLED:-false}" CAMERA_VIDEO_DEVICE="${CAMERA_VIDEO_DEVICE:-/dev/video0}" CAMERA_SIGNALING_URL="${CAMERA_SIGNALING_URL:-ws://127.0.0.1:3000}" KASMVNC_GEOMETRY="${KASMVNC_GEOMETRY:-1600x900}" AGENT_GATEWAY_TOKEN="${AGENT_GATEWAY_TOKEN:-}" PLATFORM_AGENT_INTERACTIVE="${PLATFORM_AGENT_INTERACTIVE:-auto}" PLATFORM_INSTALLER_TUI_URL="${PLATFORM_INSTALLER_TUI_URL:-}" PLATFORM_SKIP_TUI="${PLATFORM_SKIP_TUI:-false}" PLATFORM_AGENT_REINSTALL="${PLATFORM_AGENT_REINSTALL:-false}" PLATFORM_AGENT_SETTINGS_ONLY="${PLATFORM_AGENT_SETTINGS_ONLY:-false}" STEP_CURRENT=0 STEP_TOTAL=8 TUI_SKIP_REASON="" log() { printf '%s\n' "$*" } need_cmd() { command -v "$1" >/dev/null 2>&1 } has_tty() { [ -t 0 ] || { [ -r /dev/tty ] && [ -w /dev/tty ]; } } tty_available() { ( : < /dev/tty > /dev/tty ) 2>/dev/null } progress_bar() { current="$1" total="$2" label="$3" width=24 filled=$((current * width / total)) empty=$((width - filled)) bar="" i=0 while [ "$i" -lt "$filled" ]; do bar="${bar}#"; i=$((i + 1)); done i=0 while [ "$i" -lt "$empty" ]; do bar="${bar}-"; i=$((i + 1)); done printf '[%s] %s/%s %s\n' "$bar" "$current" "$total" "$label" } run_step() { STEP_CURRENT=$((STEP_CURRENT + 1)) progress_bar "$STEP_CURRENT" "$STEP_TOTAL" "$1" shift "$@" } as_root() { if [ "$(id -u)" -eq 0 ]; then "$@" elif need_cmd sudo; then sudo "$@" else log "Root privileges are required: $*" exit 1 fi } prompt_yes_no() { label="$1" default="${2:-no}" suffix="[y/N]" [ "$default" = "yes" ] && suffix="[Y/n]" if tty_available; then printf '%s %s ' "$label" "$suffix" > /dev/tty IFS= read -r answer < /dev/tty || answer="" else printf '%s %s ' "$label" "$suffix" IFS= read -r answer || answer="" fi answer="$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')" if [ -z "$answer" ]; then [ "$default" = "yes" ] return fi [ "$answer" = "y" ] || [ "$answer" = "yes" ] } agent_already_installed() { [ -f "/etc/systemd/system/${SERVICE_NAME}.service" ] || [ -f "${INSTALL_DIR}/agent.config.json" ] } handle_existing_install() { case "$PLATFORM_AGENT_REINSTALL" in true|1|yes|y|on) return ;; esac case "$PLATFORM_AGENT_SETTINGS_ONLY" in true|1|yes|y|on) if [ -x "${INSTALL_DIR}/scripts/setting.sh" ]; then exec "${INSTALL_DIR}/scripts/setting.sh" fi exec "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/setting.sh" ;; esac if ! agent_already_installed; then return fi log "Autonomy Studio Platform Agent is already installed." log "Install dir : ${INSTALL_DIR}" log "Service : ${SERVICE_NAME}" log "" if ! interactive_allowed; then log "Set PLATFORM_AGENT_REINSTALL=true to reinstall, or PLATFORM_AGENT_SETTINGS_ONLY=true to edit settings." exit 0 fi log "1. Open settings" log "2. Reinstall / update" log "3. Cancel" log "" if tty_available; then printf 'Select [1]: ' > /dev/tty IFS= read -r choice < /dev/tty || choice="" else printf 'Select [1]: ' IFS= read -r choice || choice="" fi case "${choice:-1}" in 1) if [ -x "${INSTALL_DIR}/scripts/setting.sh" ]; then exec "${INSTALL_DIR}/scripts/setting.sh" fi exec "$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)/setting.sh" ;; 2) if prompt_yes_no "Continue reinstall/update?" "no"; then return fi log "Canceled." exit 0 ;; *) log "Canceled." exit 0 ;; esac } install_bun() { if need_cmd bun; then return fi log "Installing Bun." curl -fsSL https://bun.sh/install | bash export PATH="$HOME/.bun/bin:$PATH" } interactive_allowed() { case "$PLATFORM_AGENT_INTERACTIVE" in false|0|no|n|off) return 1 ;; true|1|yes|y|on) has_tty return ;; *) has_tty return ;; esac } prompt_value() { var_name="$1" label="$2" current_value="$3" secret="${4:-false}" if [ "$secret" = "true" ]; then if tty_available; then printf '%s [%s]: ' "$label" "hidden" > /dev/tty stty -echo < /dev/tty || true IFS= read -r answer < /dev/tty || answer="" stty echo < /dev/tty || true printf '\n' > /dev/tty else printf '%s [%s]: ' "$label" "hidden" stty -echo || true IFS= read -r answer || answer="" stty echo || true printf '\n' fi else if tty_available; then printf '%s [%s]: ' "$label" "$current_value" > /dev/tty IFS= read -r answer < /dev/tty || answer="" else printf '%s [%s]: ' "$label" "$current_value" IFS= read -r answer || answer="" fi fi if [ -n "$answer" ]; then eval "$var_name=\"\$answer\"" fi } run_shell_tui() { if ! interactive_allowed; then return 1 fi clear log "┌────────────────────────────────────────────────────────────────────┐" log "│ Autonomy Studio Platform Agent Installer │" log "└────────────────────────────────────────────────────────────────────┘" log "" prompt_value PLATFORM_ACCOUNT "Autonomy Studio account" "$PLATFORM_ACCOUNT" prompt_value PLATFORM_REGISTRATION_CODE "6-digit registration code" "$PLATFORM_REGISTRATION_CODE" prompt_value WORKSPACE_ROOT "Workspace root" "$WORKSPACE_ROOT" prompt_value ROS_DOMAIN_ID "ROS_DOMAIN_ID" "$ROS_DOMAIN_ID" prompt_value CAMERA_ENABLED "Enable camera true/false" "$CAMERA_ENABLED" prompt_value CAMERA_VIDEO_DEVICE "Camera device" "$CAMERA_VIDEO_DEVICE" prompt_value CAMERA_SIGNALING_URL "Camera signaling URL" "$CAMERA_SIGNALING_URL" prompt_value KASMVNC_GEOMETRY "KasmVNC resolution" "$KASMVNC_GEOMETRY" provision_tailscale_from_backend return 0 } run_bubbletea_tui() { case "$PLATFORM_SKIP_TUI" in true|1|yes|y|on) TUI_SKIP_REASON="PLATFORM_SKIP_TUI is enabled." return 1 ;; esac if ! interactive_allowed; then TUI_SKIP_REASON="Interactive terminal is not available or PLATFORM_AGENT_INTERACTIVE=false." return 1 fi env_file="$(mktemp)" tui_bin="" current_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd || printf '')" tui_arch="" case "$(uname -m 2>/dev/null || true)" in x86_64|amd64) tui_arch="amd64" ;; aarch64|arm64) tui_arch="arm64" ;; esac if [ -n "$PLATFORM_INSTALLER_TUI_URL" ]; then tui_url="$PLATFORM_INSTALLER_TUI_URL" if [ -n "$tui_arch" ]; then tui_url="$(printf '%s' "$tui_url" | sed "s/{arch}/$tui_arch/g")" fi tui_bin="$(mktemp)" curl -fsSL "$tui_url" -o "$tui_bin" chmod +x "$tui_bin" set +e if tty_available; then "$tui_bin" "$env_file" < /dev/tty > /dev/tty else "$tui_bin" "$env_file" fi status=$? set -e if [ "$status" -ne 0 ]; then if [ "$status" -eq 130 ]; then rm -f "$env_file" "$tui_bin" exit 130 fi TUI_SKIP_REASON="Failed to run the TUI downloaded from PLATFORM_INSTALLER_TUI_URL." rm -f "$env_file" "$tui_bin" return 1 fi elif [ -n "$current_dir" ] && [ -n "$tui_arch" ] && [ -x "$current_dir/scripts/installer-tui/installer-tui-linux-$tui_arch" ]; then set +e if tty_available; then "$current_dir/scripts/installer-tui/installer-tui-linux-$tui_arch" "$env_file" < /dev/tty > /dev/tty else "$current_dir/scripts/installer-tui/installer-tui-linux-$tui_arch" "$env_file" fi status=$? set -e if [ "$status" -ne 0 ]; then if [ "$status" -eq 130 ]; then rm -f "$env_file" exit 130 fi TUI_SKIP_REASON="Failed to run the local installer-tui binary." rm -f "$env_file" return 1 fi elif [ -n "$current_dir" ] && [ -x "$current_dir/scripts/installer-tui/installer-tui" ]; then # Compatibility with existing single-architecture installer packages. set +e if tty_available; then "$current_dir/scripts/installer-tui/installer-tui" "$env_file" < /dev/tty > /dev/tty else "$current_dir/scripts/installer-tui/installer-tui" "$env_file" fi status=$? set -e if [ "$status" -ne 0 ]; then if [ "$status" -eq 130 ]; then rm -f "$env_file" exit 130 fi TUI_SKIP_REASON="Failed to run the local installer-tui binary." rm -f "$env_file" return 1 fi elif [ -n "$current_dir" ] && [ -f "$current_dir/scripts/installer-tui/main.go" ] && need_cmd go; then set +e if tty_available; then (cd "$current_dir/scripts/installer-tui" && go run . "$env_file") < /dev/tty > /dev/tty else (cd "$current_dir/scripts/installer-tui" && go run . "$env_file") fi status=$? set -e if [ "$status" -ne 0 ]; then if [ "$status" -eq 130 ]; then rm -f "$env_file" exit 130 fi TUI_SKIP_REASON="Failed to run the Bubble Tea TUI with go run." rm -f "$env_file" return 1 fi else TUI_SKIP_REASON="No TUI binary or Go runtime found. curl | sh installs require PLATFORM_INSTALLER_TUI_URL." rm -f "$env_file" return 1 fi # shellcheck disable=SC1090 . "$env_file" INSTALL_DIR="${PLATFORM_AGENT_DIR:-$INSTALL_DIR}" SERVICE_NAME="${PLATFORM_AGENT_SERVICE:-$SERVICE_NAME}" REPO_URL="${PLATFORM_AGENT_REPO_URL:-$REPO_URL}" RUN_USER="${PLATFORM_AGENT_USER:-$RUN_USER}" rm -f "$env_file" [ -n "${tui_bin:-}" ] && rm -f "$tui_bin" return 0 } collect_install_config() { if run_bubbletea_tui; then return fi if run_shell_tui; then return fi if ! interactive_allowed; then return fi log "Bubble Tea TUI is unavailable. Falling back to shell prompts." if [ -n "$TUI_SKIP_REASON" ]; then log "Reason: $TUI_SKIP_REASON" fi prompt_value PLATFORM_ACCOUNT "Autonomy Studio account" "$PLATFORM_ACCOUNT" prompt_value PLATFORM_REGISTRATION_CODE "6-digit registration code" "$PLATFORM_REGISTRATION_CODE" prompt_value WORKSPACE_ROOT "Workspace root" "$WORKSPACE_ROOT" prompt_value ROS_DOMAIN_ID "ROS_DOMAIN_ID" "$ROS_DOMAIN_ID" prompt_value CAMERA_ENABLED "Enable camera true/false" "$CAMERA_ENABLED" prompt_value CAMERA_VIDEO_DEVICE "Camera device" "$CAMERA_VIDEO_DEVICE" prompt_value CAMERA_SIGNALING_URL "Camera signaling URL" "$CAMERA_SIGNALING_URL" prompt_value KASMVNC_GEOMETRY "KasmVNC resolution" "$KASMVNC_GEOMETRY" provision_tailscale_from_backend } provision_tailscale_from_backend() { if [ -z "$PLATFORM_ACCOUNT" ] || [ -z "$PLATFORM_REGISTRATION_CODE" ]; then log "Provisioning requires both account and registration code." exit 1 fi if [ -z "$PLATFORM_PROVISION_URL" ]; then log "PLATFORM_PROVISION_URL is required for account provisioning." exit 1 fi if ! need_cmd curl; then log "Provisioning requires curl." exit 1 fi if ! need_cmd python3; then log "Shell fallback provisioning requires python3. Use the TUI binary or pass environment variables directly." exit 1 fi body_file="$(mktemp)" resp_file="$(mktemp)" env_file="$(mktemp)" legacy_body_file="$(mktemp)" chmod 600 "$body_file" "$legacy_body_file" "$resp_file" "$env_file" PLATFORM_ACCOUNT="$PLATFORM_ACCOUNT" PLATFORM_REGISTRATION_CODE="$PLATFORM_REGISTRATION_CODE" RUN_USER="$RUN_USER" WORKSPACE_ROOT="$WORKSPACE_ROOT" python3 -c 'import json, os, platform hostname = os.uname().nodename distro = platform.platform() ssh_user = os.environ.get("RUN_USER") or os.environ.get("USER") or "" workspace_root = os.environ.get("WORKSPACE_ROOT") or "~/autonomy-studio/workspaces" try: with open("/etc/os-release", "r", encoding="utf-8") as fp: for line in fp: if line.startswith("PRETTY_NAME="): distro = line.split("=", 1)[1].strip().strip("\"") break except OSError: pass device = { "hostname": hostname, "os": os.uname().sysname.lower(), "arch": {"x86_64": "amd64", "AMD64": "amd64", "aarch64": "arm64", "armv8": "arm64"}.get(platform.machine(), platform.machine()), "kernel": os.uname().release, "distro": distro, "sshUser": ssh_user, "workspaceRoot": workspace_root, } print(json.dumps({ "account": os.environ["PLATFORM_ACCOUNT"], "userCode": os.environ["PLATFORM_REGISTRATION_CODE"], "hostname": hostname, "os": device["os"], "arch": device["arch"], "kernel": device["kernel"], "distro": device["distro"], "sshUser": ssh_user, "workspaceRoot": workspace_root, "device": device, }))' > "$body_file" PLATFORM_ACCOUNT="$PLATFORM_ACCOUNT" PLATFORM_REGISTRATION_CODE="$PLATFORM_REGISTRATION_CODE" RUN_USER="$RUN_USER" WORKSPACE_ROOT="$WORKSPACE_ROOT" python3 -c 'import json, os hostname = os.uname().nodename print(json.dumps({ "account": os.environ["PLATFORM_ACCOUNT"], "userCode": os.environ["PLATFORM_REGISTRATION_CODE"], "hostname": hostname, "sshUser": os.environ.get("RUN_USER") or os.environ.get("USER") or "", "workspaceRoot": os.environ.get("WORKSPACE_ROOT") or "~/autonomy-studio/workspaces", }))' > "$legacy_body_file" http_status="$(curl -sS -H "Content-Type: application/json" --data-binary "@$body_file" "$PLATFORM_PROVISION_URL/device-authorizations/consume" -o "$resp_file" -w "%{http_code}" || printf '000')" if [ "$http_status" = "400" ]; then http_status="$(curl -sS -H "Content-Type: application/json" --data-binary "@$legacy_body_file" "$PLATFORM_PROVISION_URL/device-authorizations/consume" -o "$resp_file" -w "%{http_code}" || printf '000')" fi if [ "$http_status" -lt 200 ] || [ "$http_status" -ge 300 ]; then rm -f "$body_file" "$legacy_body_file" "$resp_file" "$env_file" log "Provisioning request failed." exit 1 fi python3 - "$resp_file" > "$env_file" <<'PY' import json import shlex import sys with open(sys.argv[1], "r", encoding="utf-8") as fp: data = json.load(fp) def first(*keys): for key in keys: value = data.get(key) if value is not None and value != "": if isinstance(value, bool): return "true" if value else "false" return str(value) return "" values = { "INSTALL_TAILSCALE": first("installTailscale", "install_tailscale", "tailscaleEnabled", "tailscale_enabled"), "TAILSCALE_AUTH_KEY": first("tailscaleAuthKey", "tailscale_auth_key", "authKey", "auth_key"), "TAILSCALE_LOGIN_SERVER": first("tailscaleLoginServer", "tailscale_login_server", "loginServer", "login_server"), "TAILSCALE_HOSTNAME": first("tailscaleHostname", "tailscale_hostname", "hostname"), "TAILSCALE_EXTRA_ARGS": first("tailscaleExtraArgs", "tailscale_extra_args", "extraArgs", "extra_args"), "AGENT_GATEWAY_TOKEN": first("agentGatewayToken", "agent_gateway_token", "gatewayToken", "gateway_token"), "AGENT_DEVICE_ID": first("agentDeviceId", "agent_device_id"), } if not values["INSTALL_TAILSCALE"] and values["TAILSCALE_AUTH_KEY"]: values["INSTALL_TAILSCALE"] = "true" for key, value in values.items(): if value: print(f"{key}={shlex.quote(value)}") PY # shellcheck disable=SC1090 . "$env_file" rm -f "$body_file" "$legacy_body_file" "$resp_file" "$env_file" } install_tailscale() { case "$INSTALL_TAILSCALE" in true|1|yes|y|on) ;; *) return ;; esac if [ "$(uname -s)" != "Linux" ]; then log "Automatic Tailscale installation is supported only on Linux. Skipping." return fi if ! need_cmd tailscale; then log "Installing Tailscale." curl -fsSL https://tailscale.com/install.sh | sh fi if need_cmd systemctl; then as_root systemctl enable --now tailscaled elif need_cmd service; then as_root service tailscaled start || true fi if [ -n "$TAILSCALE_AUTH_KEY" ]; then log "Joining the Tailscale network." set -- up --auth-key "$TAILSCALE_AUTH_KEY" if [ -n "$TAILSCALE_LOGIN_SERVER" ]; then set -- "$@" --login-server "$TAILSCALE_LOGIN_SERVER" fi if [ -n "$TAILSCALE_HOSTNAME" ]; then set -- "$@" --hostname "$TAILSCALE_HOSTNAME" fi if [ -n "$TAILSCALE_EXTRA_ARGS" ]; then # shellcheck disable=SC2086 set -- "$@" $TAILSCALE_EXTRA_ARGS fi tailscale_error_file="$(mktemp)" if ! as_root tailscale "$@" 2>"$tailscale_error_file"; then tailscale_error="$(cat "$tailscale_error_file" 2>/dev/null || true)" rm -f "$tailscale_error_file" case "$tailscale_error" in *"requires mentioning all"*|*"--reset"*) log "Existing Tailscale settings detected. Retrying with --reset." set -- "$@" --reset as_root tailscale "$@" ;; *) printf '%s\n' "$tailscale_error" >&2 exit 1 ;; esac else rm -f "$tailscale_error_file" fi else log "TAILSCALE_AUTH_KEY is empty. tailscaled was enabled but not joined." fi if [ "$RUN_USER" != "root" ]; then log "Allowing ${RUN_USER} to run Tailscale without root." as_root tailscale set --operator="$RUN_USER" || log "Failed to set Tailscale operator for ${RUN_USER}." fi } install_openssh_server() { case "$INSTALL_OPENSSH_SERVER" in true|1|yes|y|on) ;; *) return ;; esac if [ "$(uname -s)" != "Linux" ]; then log "Automatic OpenSSH server installation is supported only on Linux. Skipping." return fi if need_cmd sshd || [ -x /usr/sbin/sshd ]; then log "OpenSSH server is already installed." elif need_cmd apt-get; then log "Installing OpenSSH server with apt." as_root apt-get update as_root apt-get install -y openssh-server elif need_cmd dnf; then log "Installing OpenSSH server with dnf." as_root dnf install -y openssh-server elif need_cmd yum; then log "Installing OpenSSH server with yum." as_root yum install -y openssh-server elif need_cmd pacman; then log "Installing OpenSSH server with pacman." as_root pacman -Sy --noconfirm openssh elif need_cmd zypper; then log "Installing OpenSSH server with zypper." as_root zypper --non-interactive install openssh else log "No supported package manager found. Install OpenSSH server manually." return fi if need_cmd systemctl; then if systemctl list-unit-files ssh.service >/dev/null 2>&1; then as_root systemctl enable --now ssh elif systemctl list-unit-files sshd.service >/dev/null 2>&1; then as_root systemctl enable --now sshd else as_root systemctl restart ssh || as_root systemctl restart sshd || true fi elif need_cmd service; then as_root service ssh start || as_root service sshd start || true fi } install_git() { case "$INSTALL_GIT" in true|1|yes|y|on) ;; *) return ;; esac if need_cmd git; then log "Git is already installed." return fi if [ "$(uname -s)" != "Linux" ]; then log "Automatic Git installation is supported only on Linux. Skipping." return fi if need_cmd apt-get; then log "Installing Git with apt." as_root apt-get update as_root apt-get install -y git elif need_cmd dnf; then log "Installing Git with dnf." as_root dnf install -y git elif need_cmd yum; then log "Installing Git with yum." as_root yum install -y git elif need_cmd pacman; then log "Installing Git with pacman." as_root pacman -Sy --noconfirm git elif need_cmd zypper; then log "Installing Git with zypper." as_root zypper --non-interactive install git else log "No supported package manager found. Install Git manually." fi } prepare_sources() { if [ -n "$REPO_URL" ]; then as_root mkdir -p "$INSTALL_DIR" if [ -d "$INSTALL_DIR/.git" ]; then as_root git -C "$INSTALL_DIR" pull --ff-only else tmp_dir="$(mktemp -d)" git clone "$REPO_URL" "$tmp_dir" as_root cp -a "$tmp_dir/." "$INSTALL_DIR/" rm -rf "$tmp_dir" fi else current_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" as_root mkdir -p "$INSTALL_DIR" as_root cp -a "$current_dir/." "$INSTALL_DIR/" fi as_root chown -R "$RUN_USER":"$RUN_USER" "$INSTALL_DIR" } install_dependencies() { (cd "$INSTALL_DIR" && bun install) } write_agent_config_from_env() { ( cd "$INSTALL_DIR" AGENT_CONFIG_PATH="$INSTALL_DIR/agent.config.json" \ WORKSPACE_ROOT="$WORKSPACE_ROOT" \ ROBOT_STACK="$ROBOT_STACK" \ ROS_DOMAIN_ID="$ROS_DOMAIN_ID" \ CAMERA_ENABLED="$CAMERA_ENABLED" \ CAMERA_VIDEO_DEVICE="$CAMERA_VIDEO_DEVICE" \ CAMERA_SIGNALING_URL="$CAMERA_SIGNALING_URL" \ KASMVNC_GEOMETRY="$KASMVNC_GEOMETRY" \ AGENT_GATEWAY_TOKEN="$AGENT_GATEWAY_TOKEN" \ AGENT_DEVICE_ID="$AGENT_DEVICE_ID" \ bun -e ' const fs = require("fs"); const path = process.env.AGENT_CONFIG_PATH; const cfg = JSON.parse(fs.readFileSync(path, "utf8")); cfg.security ??= {}; cfg.workspace ??= {}; cfg.ros ??= {}; cfg.camera ??= {}; cfg.desktop ??= {}; cfg.security.gatewayToken = process.env.AGENT_GATEWAY_TOKEN || cfg.security.gatewayToken || ""; cfg.updates ??= {}; cfg.updates.enabled = cfg.updates.enabled !== false; cfg.updates.checkUrl = cfg.updates.checkUrl || "https://portal-api.autonomystudio.net/api/provision/agent/update-check"; cfg.updates.deviceId = process.env.AGENT_DEVICE_ID || cfg.updates.deviceId || ""; cfg.workspace.root = process.env.WORKSPACE_ROOT || cfg.workspace.root || "~/autonomy-studio/workspaces"; cfg.ros.stack = process.env.ROBOT_STACK === "nav2" ? "nav2" : "autoware"; cfg.ros.domainId = /^\d+$/.test(process.env.ROS_DOMAIN_ID || "") ? process.env.ROS_DOMAIN_ID : "0"; cfg.camera.enabled = /^(true|1|yes|y|on)$/i.test(process.env.CAMERA_ENABLED || ""); cfg.camera.videoDevice = process.env.CAMERA_VIDEO_DEVICE || cfg.camera.videoDevice || "/dev/video0"; cfg.camera.signalingUrl = process.env.CAMERA_SIGNALING_URL || cfg.camera.signalingUrl || "ws://127.0.0.1:3000"; cfg.desktop.geometry = process.env.KASMVNC_GEOMETRY || cfg.desktop.geometry || "1600x900"; fs.writeFileSync(path, JSON.stringify(cfg, null, 2) + "\n"); ' ) } write_service() { service_path="/etc/systemd/system/${SERVICE_NAME}.service" tmp_service="$(mktemp)" bun_bin="$(command -v bun)" cat > "$tmp_service" <:3100" if need_cmd tailscale; then ts_ip="$(tailscale ip -4 2>/dev/null | head -n 1 || true)" if [ -n "$ts_ip" ]; then log "Tailscale Settings UI: http://${ts_ip}:3100" fi fi log "" log "Automatic Tailscale install example:" log "INSTALL_TAILSCALE=true TAILSCALE_AUTH_KEY=tskey-auth-... sh scripts/install.sh" log "" log "Optional host dependencies:" log "- KasmVNC/XFCE: vncserver or kasmvncserver, XFCE" log "- Camera: GStreamer, v4l-utils, python3-gi, websockets" log "- ROS bridge: ROS2 Humble, FastAPI requirements, Autoware/Nav2 message packages" log "- Docker module runtime: docker, nvidia-container-toolkit, or the device-specific GPU runtime" } handle_existing_install collect_install_config run_step "Install Tailscale if requested" install_tailscale run_step "Install OpenSSH server if needed" install_openssh_server run_step "Install Git if needed" install_git run_step "Install Bun if needed" install_bun run_step "Prepare sources" prepare_sources run_step "Install dependencies" install_dependencies run_step "Write agent config" write_agent_config_from_env run_step "Install systemd service" write_service print_host_notes