Files
agent/scripts/install.sh
T
2026-08-07 10:14:19 +09:00

785 lines
25 KiB
Bash
Executable File

#!/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" <<EOF
[Unit]
Description=Platform Agent
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=${RUN_USER}
WorkingDirectory=${INSTALL_DIR}
ExecStart=${bun_bin} src/server.tsx
Restart=always
RestartSec=2
Environment=HOST=0.0.0.0
Environment=AGENT_CONFIG=${INSTALL_DIR}/agent.config.json
Environment=AGENT_SSH_USER=${RUN_USER}
[Install]
WantedBy=multi-user.target
EOF
as_root cp "$tmp_service" "$service_path"
rm -f "$tmp_service"
as_root systemctl daemon-reload
as_root systemctl enable "$SERVICE_NAME"
as_root systemctl restart "$SERVICE_NAME"
}
print_host_notes() {
log ""
log "Install complete: ${INSTALL_DIR}"
log "Service: systemctl status ${SERVICE_NAME}"
log "Settings UI: http://<device-ip>: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