Files
Drew Newberry a67567e583 fix(snap): require mTLS for the snap gateway (#3726)
* fix(snap): require mTLS for the snap gateway

Replace the installer opt-in with an authenticated snap gateway. The wrapper
no longer forces plaintext, so the gateway serves TLS from the bundle it
already generates in $SNAP_COMMON/tls. The install hook writes a config that
enables mTLS user auth instead of unauthenticated access, and a new
post-refresh hook migrates the exact legacy default on existing installs.

install.sh waits for the gateway, detects whether it serves TLS, copies the
client bundle into the target user's snap state directory, and registers the
gateway over HTTPS. Older plaintext snap revisions still register over HTTP
with a warning. The release canary asserts mTLS auth and HTTPS registration.

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(snap): pass config preflight and detect the mTLS gateway reliably

An explicit [openshell.gateway.mtls_auth] table fails config preflight,
which validates mTLS auth before the local TLS bundle supplies the client
CA. Write a default that pins the Docker driver instead; with the wrapper's
TLS bundle the gateway requires client certificates and enables mTLS user
auth automatically, as the native packages do.

The mTLS gateway rejects TLS handshakes without a client certificate, and
it still answers plaintext loopback HTTP for sandbox service routing, so the
installer could misdetect it as a legacy plaintext gateway. Probe HTTPS with
the root-owned client bundle, and treat a gateway as legacy only when a
plaintext gRPC Health call succeeds.

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* chore(snap): simplify install hook comment

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(snap): migrate insecure gateway configs on refresh

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* refactor(snap): simplify mTLS detection and config migration

Detect the mTLS snap from the installed revision's post-refresh hook instead
of probing plaintext gRPC, and drop the scheme global. Remove the installer's
pre-hook config fallback, which is dead now that every channel ships the
install hook and which wrote the insecure default. Give the install hook a
single write path with a simple backup name, and shorten the manual client
certificate steps.

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(snap): stop keeping a copy of replaced insecure configs

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* feat(install): make the snap an opt-in install method

Stop selecting the OpenShell snap just because the snap command exists. Linux
installs default to the Debian or RPM package; OPENSHELL_INSTALL_METHOD=snap
(or deb, rpm) selects the package explicitly. Hosts that already have the
OpenShell snap keep refreshing it rather than gaining a second gateway on the
same port. The release canary and snap repro script opt in explicitly.

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(snap): let the gateway auto-detect its compute driver

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* fix(snap): restart the gateway after refresh

Published revisions use refresh-mode: endure, and snapd honors the old
revision's setting during a refresh, so the plaintext gateway kept running
with the migrated config unused until a manual restart. Restart the gateway
from the post-refresh hook so the mTLS config takes effect immediately.

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(snap): drop refresh notes from the snap description

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

* docs(snap): trim snap refresh notes from installation docs

Signed-off-by: Drew Newberry <anewberry@nvidia.com>

---------

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
2026-09-26 01:50:02 +00:00

1498 lines
44 KiB
Bash
Executable File

#!/bin/sh
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Install OpenShell from a GitHub release.
#
# Linux installs either the Debian or RPM packages from the selected release.
# Apple Silicon macOS installs the generated Homebrew formula, so Homebrew owns
# the binary layout and launchd service lifecycle.
#
set -e
APP_NAME="openshell"
REPO="NVIDIA/OpenShell"
GITHUB_URL="https://github.com/${REPO}"
RELEASE_TAG="${OPENSHELL_VERSION:-}"
RELEASE_ASSET_DIR=""
CHECKSUMS_NAME="openshell-checksums-sha256.txt"
LOCAL_GATEWAY_PORT="17670"
HOMEBREW_TAP="nvidia/openshell"
HOMEBREW_FORMULA_NAME="openshell"
HOMEBREW_CLI_ASSET="openshell-aarch64-apple-darwin.tar.gz"
HOMEBREW_GATEWAY_ASSET="openshell-gateway-aarch64-apple-darwin.tar.gz"
HOMEBREW_DRIVER_VM_ASSET="openshell-driver-vm-aarch64-apple-darwin.tar.gz"
HOMEBREW_PROVER_ASSET="openshell-prover-aarch64-apple-darwin.tar.gz"
BREAKING_RELEASE_VERSION="0.0.37"
LINUX_PACKAGE_GLIBC_MIN_VERSION="2.28"
UPGRADE_NOTICE_ACK="${OPENSHELL_ACK_BREAKING_UPGRADE:-}"
info() {
printf '%s: %s\n' "$APP_NAME" "$*" >&2
}
warn() {
printf '%s: warning: %s\n' "$APP_NAME" "$*" >&2
}
error() {
printf '%s: error: %s\n' "$APP_NAME" "$*" >&2
exit 1
}
usage() {
cat <<EOF
install.sh - Install OpenShell
USAGE:
curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh -o install.sh
sh install.sh
curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
OPTIONS:
--help Print this help message
ENVIRONMENT VARIABLES:
OPENSHELL_VERSION Release tag to install (default: latest tagged release).
Set OPENSHELL_VERSION=dev to install the rolling dev build.
Set OPENSHELL_VERSION=pre to install the latest prerelease.
Prereleases require an authenticated GitHub CLI session.
OPENSHELL_ACK_BREAKING_UPGRADE
Set to 1 only after backing up and cleaning up a
pre-v0.0.37 or non-snap installation.
OPENSHELL_INSTALL_METHOD
Linux package to install: snap, deb, or rpm. Unset
selects deb or rpm from the host package manager.
NOTES:
When OPENSHELL_VERSION is unset, this resolves the latest tagged release
from ${GITHUB_URL}/releases/latest.
Linux installs the Debian package on amd64/arm64 or the RPM packages on
x86_64/aarch64, depending on the host package manager. Set
OPENSHELL_INSTALL_METHOD=snap to install the OpenShell snap instead; hosts
that already have the OpenShell snap keep refreshing it. Snap installs use
latest/stable by default and latest/edge for dev, and do not support
explicit release tags or prereleases. The OpenShell snap requires a running
Docker Engine installed from a system package or Docker's package
repository. The Docker snap is not currently compatible with OpenShell.
macOS installs the release Homebrew formula on Apple Silicon and starts a
brew services-backed local gateway.
EOF
}
has_cmd() {
command -v "$1" >/dev/null 2>&1
}
require_cmd() {
if ! has_cmd "$1"; then
error "'$1' is required"
fi
}
download() {
_url="$1"
_output="$2"
curl -fLsS --retry 3 --max-redirs 5 -o "$_output" "$_url"
}
semver_core() {
_version="${1#v}"
_version="${_version%%[-+]*}"
printf '%s\n' "$_version"
}
semver_at_least() {
_version="$(semver_core "$1")"
_minimum="$(semver_core "$2")"
_major="${_version%%.*}"
_rest="${_version#*.}"
[ "$_rest" != "$_version" ] || return 1
_minor="${_rest%%.*}"
_patch="${_rest#*.}"
_patch="${_patch%%.*}"
_min_major="${_minimum%%.*}"
_min_rest="${_minimum#*.}"
[ "$_min_rest" != "$_minimum" ] || return 1
_min_minor="${_min_rest%%.*}"
_min_patch="${_min_rest#*.}"
_min_patch="${_min_patch%%.*}"
case "$_major:$_minor:$_patch:$_min_major:$_min_minor:$_min_patch" in
*[!0-9:]* | *::*)
return 1
;;
esac
[ "$_major" -gt "$_min_major" ] && return 0
[ "$_major" -lt "$_min_major" ] && return 1
[ "$_minor" -gt "$_min_minor" ] && return 0
[ "$_minor" -lt "$_min_minor" ] && return 1
[ "$_patch" -ge "$_min_patch" ]
}
version_at_least_major_minor() {
_version="$1"
_minimum="$2"
_major="${_version%%.*}"
_minor="${_version#*.}"
_minor="${_minor%%.*}"
_min_major="${_minimum%%.*}"
_min_minor="${_minimum#*.}"
_min_minor="${_min_minor%%.*}"
case "$_major:$_minor:$_min_major:$_min_minor" in
*[!0-9:]* | *::*)
return 1
;;
esac
[ "$_major" -gt "$_min_major" ] && return 0
[ "$_major" -lt "$_min_major" ] && return 1
[ "$_minor" -ge "$_min_minor" ]
}
getconf_gnu_libc_version() {
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_GETCONF_UNAVAILABLE:-0}" = "1" ]; then
return 127
fi
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_GETCONF_OUTPUT+x}" = "x" ]; then
printf '%s\n' "$OPENSHELL_TEST_GETCONF_OUTPUT"
return 0
fi
getconf GNU_LIBC_VERSION 2>/dev/null
}
ldd_version_output() {
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_LDD_UNAVAILABLE:-0}" = "1" ]; then
return 127
fi
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" = "1" ] && [ "${OPENSHELL_TEST_LDD_OUTPUT+x}" = "x" ]; then
printf '%s\n' "$OPENSHELL_TEST_LDD_OUTPUT"
return 0
fi
ldd --version 2>&1
}
detect_glibc_version() {
_ldd_output="$(ldd_version_output 2>&1 || true)"
case "$_ldd_output" in
*[Mm][Uu][Ss][Ll]* | *[Aa][Ll][Pp][Ii][Nn][Ee]*)
return 2
;;
esac
_ldd_version="$(printf '%s\n' "$_ldd_output" | awk 'FNR == 1 && match($NF, /^[0-9]+\.[0-9]+/) { print substr($NF, RSTART, RLENGTH); exit }')"
if [ -n "$_ldd_version" ]; then
printf '%s\n' "$_ldd_version"
return 0
fi
_getconf_output="$(getconf_gnu_libc_version 2>/dev/null || true)"
case "$_getconf_output" in
glibc\ [0-9]*.[0-9]*)
printf '%s\n' "${_getconf_output#glibc }"
return 0
;;
*[Mm][Uu][Ss][Ll]* | *[Aa][Ll][Pp][Ii][Nn][Ee]*)
return 2
;;
esac
return 1
}
require_linux_package_glibc() {
_glibc_status=0
_glibc_version="$(detect_glibc_version)" || _glibc_status=$?
case "$_glibc_status" in
0)
;;
2)
error "OpenShell Linux packages require glibc >= ${LINUX_PACKAGE_GLIBC_MIN_VERSION}; detected musl or unsupported libc."
;;
*)
error "OpenShell Linux packages require glibc >= ${LINUX_PACKAGE_GLIBC_MIN_VERSION}; could not detect glibc."
;;
esac
if ! version_at_least_major_minor "$_glibc_version" "$LINUX_PACKAGE_GLIBC_MIN_VERSION"; then
error "OpenShell Linux packages require glibc >= ${LINUX_PACKAGE_GLIBC_MIN_VERSION}; detected glibc ${_glibc_version}.
Please use a newer distribution or container environment."
fi
}
target_uses_breaking_gateway_model() {
case "$RELEASE_TAG" in
dev)
return 0
;;
esac
semver_at_least "$RELEASE_TAG" "$BREAKING_RELEASE_VERSION"
}
installed_version_needs_breaking_upgrade_notice() {
_version="$1"
if [ -z "$_version" ]; then
return 0
fi
! semver_at_least "$_version" "$BREAKING_RELEASE_VERSION"
}
find_existing_native_openshell_bin() {
_path="$(command -v openshell 2>/dev/null || true)"
case "$_path" in
/snap/*) ;;
*)
if [ -n "$_path" ] && [ -x "$_path" ]; then
printf '%s\n' "$_path"
return 0
fi
;;
esac
for _candidate in \
"${TARGET_HOME:-}/.local/bin/openshell" \
/usr/local/bin/openshell \
/usr/bin/openshell \
/opt/homebrew/bin/openshell; do
if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then
printf '%s\n' "$_candidate"
return 0
fi
done
return 1
}
existing_openshell_version() {
_bin="$1"
_output="$("$_bin" --version 2>/dev/null | sed -n '1p' || true)"
printf '%s\n' "$_output" | awk '
{
for (i = 1; i <= NF; i++) {
if ($i ~ /^v?[0-9]+\.[0-9]+\.[0-9]+([-+][A-Za-z0-9.+~-]+)?$/) {
print $i
exit
}
}
}
'
}
print_breaking_upgrade_notice() {
_bin="$1"
_version="$2"
if [ -n "$_version" ]; then
warn "detected existing OpenShell ${_version} at ${_bin}"
else
warn "detected an existing OpenShell installation at ${_bin}"
fi
cat >&2 <<EOF
OpenShell ${BREAKING_RELEASE_VERSION} and later are incompatible with gateway
state created by earlier releases. Before installing OpenShell ${RELEASE_TAG}, back up
any files, artifacts, and configuration you need from existing sandboxes.
Then clean up the old runtime with the currently installed CLI:
openshell sandbox delete --all
openshell gateway destroy
Run these commands before upgrading because 'openshell gateway destroy' is not
available in OpenShell ${BREAKING_RELEASE_VERSION} and later.
After cleanup, rerun this installer or follow the installation guide:
https://docs.nvidia.com/openshell/latest/about/installation
If you have already backed up and cleaned up the old runtime, rerun with:
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_ACK_BREAKING_UPGRADE=1 sh
EOF
}
guard_breaking_upgrade() {
target_uses_breaking_gateway_model || return 0
_bin="$(find_existing_native_openshell_bin || true)"
[ -n "$_bin" ] || return 0
_version="$(existing_openshell_version "$_bin")"
installed_version_needs_breaking_upgrade_notice "$_version" || return 0
print_breaking_upgrade_notice "$_bin" "$_version"
if [ "$UPGRADE_NOTICE_ACK" = "1" ]; then
warn "continuing because OPENSHELL_ACK_BREAKING_UPGRADE=1 is set"
return 0
fi
error "manual cleanup is required before upgrading from this OpenShell installation"
}
print_native_to_snap_notice() {
_bin="$1"
_version="$2"
if [ -n "$_version" ]; then
warn "detected existing non-snap OpenShell ${_version} at ${_bin}"
else
warn "detected an existing non-snap OpenShell installation at ${_bin}"
fi
cat >&2 <<EOF
The OpenShell snap keeps gateway and CLI state in snap-specific directories and
does not import state from a non-snap installation. Before installing the snap,
back up anything you need and clean up sandboxes and runtime resources managed
by the existing installation.
For older installations that provide these commands, run:
${_bin} sandbox delete --all
${_bin} gateway destroy
Stop the non-snap gateway service and remove the native package or manual
installation before continuing. For package and service instructions, see:
https://docs.nvidia.com/openshell/latest/about/installation
If you have already backed up and cleaned up the non-snap installation, rerun with:
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_ACK_BREAKING_UPGRADE=1 sh
EOF
}
guard_native_to_snap_transition() {
_bin="$(find_existing_native_openshell_bin || true)"
[ -n "$_bin" ] || return 0
_version="$(existing_openshell_version "$_bin")"
print_native_to_snap_notice "$_bin" "$_version"
if [ "$UPGRADE_NOTICE_ACK" = "1" ]; then
warn "continuing because OPENSHELL_ACK_BREAKING_UPGRADE=1 is set"
return 0
fi
error "manual cleanup is required before replacing this non-snap OpenShell installation"
}
resolve_release_tag() {
if [ "${OPENSHELL_VERSION:-}" = "pre" ]; then
resolve_latest_prerelease_tag
return 0
fi
if [ -n "${OPENSHELL_VERSION:-}" ]; then
echo "$OPENSHELL_VERSION"
return 0
fi
info "resolving latest version..."
_latest_url="${GITHUB_URL}/releases/latest"
_resolved="$(curl -fLsS -o /dev/null -w '%{url_effective}' "$_latest_url")" || {
error "failed to resolve latest release from ${_latest_url}"
}
case "$_resolved" in
https://github.com/${REPO}/releases/*)
;;
*)
error "unexpected redirect target: ${_resolved} (expected https://github.com/${REPO}/releases/...)"
;;
esac
_version="${_resolved##*/}"
if [ -z "$_version" ] || [ "$_version" = "latest" ]; then
error "could not determine latest release version (resolved URL: ${_resolved})"
fi
echo "$_version"
}
resolve_latest_prerelease_tag() {
require_prerelease_github_access
info "resolving latest prerelease..."
_artifact_platform="$(prerelease_artifact_platform)"
_release_tags="$(gh api "repos/${REPO}/git/matching-refs/tags/v" --jq '
[.[].ref | sub("^refs/tags/"; "") |
select(test("^v[0-9]+\\.[0-9]+\\.[0-9]+-pre\\.[1-9][0-9]*$"))] |
sort_by(split("-pre.") as $parts |
($parts[0] | ltrimstr("v") | split(".") | map(tonumber)) +
[($parts[1] | tonumber)]) | reverse | .[]')" || {
error "failed to list prerelease tags"
}
for _tag in $_release_tags; do
_artifact_name="openshell-${_tag}-${_artifact_platform}"
info "checking ${_tag} for ${_artifact_platform}..."
_run_ids="$(gh api \
"repos/${REPO}/actions/artifacts?name=${_artifact_name}&per_page=100" \
--jq '[.artifacts[] | select(.expired == false)] |
sort_by(.created_at) | reverse | .[] | .workflow_run.id')" || {
error "failed to find prerelease artifact ${_artifact_name}"
}
for _run_id in $_run_ids; do
_successful="$(gh api "repos/${REPO}/actions/runs/${_run_id}" --jq '
.status == "completed" and .conclusion == "success" and
(.path | startswith(".github/workflows/release-tag.yml"))')" || {
error "failed to check prerelease workflow run ${_run_id}"
}
if [ "$_successful" = "true" ]; then
printf '%s\n' "$_tag"
return 0
fi
done
done
error "no unexpired prerelease artifacts found"
}
is_prerelease_tag() {
printf '%s\n' "$1" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$'
}
require_prerelease_github_access() {
require_cmd gh
if ! gh auth status --hostname github.com >/dev/null 2>&1; then
error "GitHub CLI authentication is required for prerelease artifacts; run 'gh auth login' and try again"
fi
}
prerelease_artifact_platform() {
case "${PLATFORM}:$(uname -m)" in
linux:x86_64 | linux:amd64)
case "$(linux_package_method)" in
deb) echo "linux-amd64-deb" ;;
rpm) echo "linux-x86_64-rpm" ;;
esac
;;
linux:aarch64 | linux:arm64)
case "$(linux_package_method)" in
deb) echo "linux-arm64-deb" ;;
rpm) echo "linux-aarch64-rpm" ;;
esac
;;
darwin:arm64 | darwin:aarch64) echo "macos-arm64" ;;
*) error "no prerelease artifact is available for ${PLATFORM}/$(uname -m)" ;;
esac
}
prepare_prerelease_assets() {
_destination="$1"
is_prerelease_tag "$RELEASE_TAG" || return 0
require_prerelease_github_access
_artifact_name="openshell-${RELEASE_TAG}-$(prerelease_artifact_platform)"
info "locating ${_artifact_name}..."
_run_id="$(gh api \
"repos/${REPO}/actions/artifacts?name=${_artifact_name}&per_page=100" \
--jq '[.artifacts[] | select(.expired == false)] | sort_by(.created_at) | last | .workflow_run.id')" || {
error "failed to find prerelease artifact ${_artifact_name}"
}
if [ -z "$_run_id" ] || [ "$_run_id" = "null" ] || ! printf '%s\n' "$_run_id" | grep -Eq '^[0-9]+$'; then
error "no unexpired prerelease artifact found for ${RELEASE_TAG} on this platform"
fi
RELEASE_ASSET_DIR="${_destination}/release"
info "downloading ${_artifact_name}..."
gh run download "$_run_id" \
--repo "$REPO" \
--name "$_artifact_name" \
--dir "$RELEASE_ASSET_DIR" || {
error "failed to download prerelease artifact ${_artifact_name}"
}
}
download_release_asset() {
_tag="$1"
_filename="$2"
_output="$3"
if [ -n "$RELEASE_ASSET_DIR" ]; then
_source="${RELEASE_ASSET_DIR}/${_filename}"
if [ -f "$_source" ]; then
cp "$_source" "$_output"
return 0
fi
return 1
fi
if curl -fLs --retry 3 --max-redirs 5 -o "$_output" \
"${GITHUB_URL}/releases/download/${_tag}/${_filename}"; then
return 0
fi
# GitHub normalizes `~` to `.` in release asset names, while checksum files
# can still record package filenames with `~dev` for correct version ordering.
# Download the normalized asset but verify it against the checksum entry for
# the original package filename.
_normalized="$(printf '%s' "$_filename" | tr '~' '.')"
if [ "$_normalized" != "$_filename" ]; then
if download "${GITHUB_URL}/releases/download/${_tag}/${_normalized}" "$_output"; then
info "using GitHub-normalized asset name ${_normalized}"
return 0
fi
fi
return 1
}
as_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
elif has_cmd sudo; then
sudo "$@"
else
error "this installer needs root privileges; rerun as root or install sudo"
fi
}
target_user() {
if [ "$(id -u)" -eq 0 ] && [ -n "${SUDO_USER:-}" ] && [ "${SUDO_USER}" != "root" ]; then
echo "$SUDO_USER"
else
id -un
fi
}
user_home() {
_user="$1"
if has_cmd getent; then
_home="$(getent passwd "$_user" | awk -F: '{ print $6 }')"
if [ -n "$_home" ]; then
echo "$_home"
return 0
fi
fi
if [ "$(uname -s)" = "Darwin" ] && has_cmd dscl; then
_home="$(dscl . -read "/Users/${_user}" NFSHomeDirectory 2>/dev/null | awk '{ print $2 }')"
if [ -n "$_home" ]; then
echo "$_home"
return 0
fi
fi
if [ "$(id -un)" = "$_user" ]; then
echo "${HOME:-}"
return 0
fi
if [ "$(uname -s)" = "Darwin" ]; then
echo "/Users/${_user}"
return 0
fi
echo "/home/${_user}"
}
as_target_user() {
if [ "${PLATFORM:-}" = "darwin" ]; then
if [ "$(id -u)" -eq "$TARGET_UID" ]; then
env HOME="$TARGET_HOME" "$@"
elif has_cmd sudo; then
sudo -u "$TARGET_USER" env HOME="$TARGET_HOME" "$@"
else
error "cannot run commands as ${TARGET_USER}; install sudo or run as ${TARGET_USER}"
fi
return
fi
_bus="unix:path=${TARGET_RUNTIME_DIR}/bus"
if [ "$(id -u)" -eq "$TARGET_UID" ]; then
env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
elif has_cmd sudo; then
sudo -u "$TARGET_USER" env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
elif has_cmd runuser; then
runuser -u "$TARGET_USER" -- env HOME="$TARGET_HOME" XDG_RUNTIME_DIR="$TARGET_RUNTIME_DIR" DBUS_SESSION_BUS_ADDRESS="$_bus" "$@"
else
error "cannot run user service commands as ${TARGET_USER}; install sudo or run as ${TARGET_USER}"
fi
}
detect_platform() {
case "$(uname -s)" in
Linux)
echo "linux"
;;
Darwin)
echo "darwin"
;;
*)
error "unsupported OS: $(uname -s); this installer supports Linux and macOS"
;;
esac
}
local_gateway_endpoint() {
case "${PLATFORM:-$(detect_platform)}" in
darwin)
printf 'https://localhost:%s\n' "$LOCAL_GATEWAY_PORT"
;;
*)
printf 'https://127.0.0.1:%s\n' "$LOCAL_GATEWAY_PORT"
;;
esac
}
linux_package_method() {
case "${OPENSHELL_INSTALL_METHOD:-}" in
snap | deb | rpm)
echo "$OPENSHELL_INSTALL_METHOD"
return 0
;;
'') ;;
*) error "unsupported OPENSHELL_INSTALL_METHOD=${OPENSHELL_INSTALL_METHOD}; use snap, deb, or rpm" ;;
esac
# Keep refreshing an existing snap install instead of adding a second
# gateway on the same port.
case "${OPENSHELL_VERSION:-}" in
'' | dev)
if has_cmd snap && snap list openshell >/dev/null 2>&1; then
echo "snap"
return 0
fi
;;
esac
if has_cmd dpkg; then
echo "deb"
elif has_cmd rpm; then
echo "rpm"
else
error "Linux installs require either dpkg or rpm"
fi
}
set_linux_target_runtime_dir() {
if [ "$(id -u)" -eq "$TARGET_UID" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then
TARGET_RUNTIME_DIR="$XDG_RUNTIME_DIR"
else
TARGET_RUNTIME_DIR="/run/user/${TARGET_UID}"
fi
}
check_linux_deb_platform() {
require_cmd dpkg
}
check_macos_platform() {
_arch="$(uname -m)"
case "$_arch" in
arm64|aarch64)
;;
x86_64|amd64)
error "Intel macOS is not supported because no x86_64-apple-darwin release assets are published"
;;
*)
error "no macOS release build is published for architecture: ${_arch}"
;;
esac
if ! as_target_user brew --version >/dev/null 2>&1; then
error "Homebrew is required for macOS installs; install it from https://brew.sh"
fi
}
get_deb_arch() {
_arch="$(dpkg --print-architecture)"
case "$_arch" in
amd64|arm64)
echo "$_arch"
;;
*)
error "no Debian package is published for architecture: ${_arch}"
;;
esac
}
get_rpm_arch() {
if has_cmd rpm; then
_arch="$(rpm --eval '%{_arch}' 2>/dev/null || true)"
else
_arch=""
fi
if [ -z "$_arch" ]; then
_arch="$(uname -m)"
fi
case "$_arch" in
x86_64|amd64)
echo "x86_64"
;;
aarch64|arm64)
echo "aarch64"
;;
*)
error "no RPM package is published for architecture: ${_arch}"
;;
esac
}
find_deb_asset() {
_checksums="$1"
_arch="$2"
awk -v arch="$_arch" '
$2 ~ "^\\*?openshell[-_].*[-_]" arch "\\.deb$" {
sub("^\\*", "", $2)
print $2
exit
}
' "$_checksums"
}
find_rpm_asset() {
_checksums="$1"
_arch="$2"
_package="$3"
case "$_package" in
openshell)
_dev_name="openshell-dev-${_arch}.rpm"
_fallback_re="^openshell-[0-9].*\\.${_arch}\\.rpm$"
;;
openshell-gateway)
_dev_name="openshell-gateway-dev-${_arch}.rpm"
_fallback_re="^openshell-gateway-[0-9].*\\.${_arch}\\.rpm$"
;;
openshell-prover)
_dev_name="openshell-prover-dev-${_arch}.rpm"
_fallback_re="^openshell-prover-[0-9].*\\.${_arch}\\.rpm$"
;;
*)
error "unknown RPM package selector: ${_package}"
;;
esac
awk -v dev_name="$_dev_name" -v fallback_re="$_fallback_re" '
{
name = $2
sub("^\\*", "", name)
if (name == dev_name) {
selected = name
found = 1
exit
}
if (fallback == "" && name ~ fallback_re) {
fallback = name
}
}
END {
if (found) {
print selected
} else if (fallback != "") {
print fallback
}
}
' "$_checksums"
}
verify_checksum() {
_archive="$1"
_checksums="$2"
_filename="$3"
if has_cmd sha256sum; then
_expected="$(awk -v name="$_filename" '($2 == name || $2 == "*" name) { print $1; exit }' "$_checksums")"
[ -n "$_expected" ] || error "no checksum entry found for ${_filename}"
echo "$_expected $_archive" | sha256sum -c --quiet
elif has_cmd shasum; then
_expected="$(awk -v name="$_filename" '($2 == name || $2 == "*" name) { print $1; exit }' "$_checksums")"
[ -n "$_expected" ] || error "no checksum entry found for ${_filename}"
echo "$_expected $_archive" | shasum -a 256 -c --quiet
else
error "neither 'sha256sum' nor 'shasum' found; cannot verify download integrity"
fi
}
install_deb_package() {
_deb_path="$1"
if has_cmd apt-get; then
as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y \
-o Dpkg::Options::=--force-confdef \
-o Dpkg::Options::=--force-confnew \
"$_deb_path"
elif has_cmd apt; then
as_root env DEBIAN_FRONTEND=noninteractive apt install -y \
-o Dpkg::Options::=--force-confdef \
-o Dpkg::Options::=--force-confnew \
"$_deb_path"
else
as_root dpkg --force-confdef --force-confnew -i "$_deb_path"
fi
}
install_rpm_packages() {
if has_cmd dnf; then
as_root dnf install -y "$@"
elif has_cmd yum; then
as_root yum install -y "$@"
elif has_cmd zypper; then
as_root zypper --non-interactive install --allow-unsigned-rpm "$@"
elif has_cmd rpm; then
warn "installing with rpm directly; dependencies must already be installed"
as_root rpm -Uvh --replacepkgs "$@"
else
error "'dnf', 'yum', 'zypper', or 'rpm' is required to install RPM packages"
fi
}
homebrew_formula_path() {
_tap="$1"
_formula="$2"
if ! as_target_user brew tap-info "$_tap" >/dev/null 2>&1; then
info "creating local Homebrew tap ${_tap}..."
as_target_user brew tap-new --no-git "$_tap" >/dev/null
fi
_tap_dir="$(as_target_user brew --repository "$_tap" 2>/dev/null || true)"
[ -n "$_tap_dir" ] || error "could not locate Homebrew tap ${_tap}"
_formula_dir="${_tap_dir}/Formula"
as_target_user mkdir -p "$_formula_dir"
printf '%s/%s.rb\n' "$_formula_dir" "$_formula"
}
patch_homebrew_formula() {
_formula_file="$1"
_patched_file="${_formula_file}.patched"
if grep -q 'entitlements.write <<~XML' "$_formula_file"; then
info "patching Homebrew formula for idempotent postinstall..."
sed 's/entitlements\.write <<~XML/entitlements.atomic_write <<~XML/' "$_formula_file" >"$_patched_file"
mv "$_patched_file" "$_formula_file"
fi
}
patch_prerelease_homebrew_formula_urls() {
_formula_file="$1"
[ -n "$RELEASE_ASSET_DIR" ] || return 0
for _asset in "$HOMEBREW_CLI_ASSET" "$HOMEBREW_GATEWAY_ASSET" "$HOMEBREW_DRIVER_VM_ASSET" "$HOMEBREW_PROVER_ASSET"; do
if [ ! -f "${RELEASE_ASSET_DIR}/${_asset}" ]; then
error "prerelease artifact is missing the required macOS asset: ${_asset}"
fi
done
_release_asset_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}"
_local_asset_url="file://${RELEASE_ASSET_DIR}"
_patched_file="${_formula_file}.prerelease"
sed \
-e "s#${_release_asset_url}/${HOMEBREW_CLI_ASSET}#${_local_asset_url}/${HOMEBREW_CLI_ASSET}#g" \
-e "s#${_release_asset_url}/${HOMEBREW_GATEWAY_ASSET}#${_local_asset_url}/${HOMEBREW_GATEWAY_ASSET}#g" \
-e "s#${_release_asset_url}/${HOMEBREW_DRIVER_VM_ASSET}#${_local_asset_url}/${HOMEBREW_DRIVER_VM_ASSET}#g" \
-e "s#${_release_asset_url}/${HOMEBREW_PROVER_ASSET}#${_local_asset_url}/${HOMEBREW_PROVER_ASSET}#g" \
"$_formula_file" >"$_patched_file"
mv "$_patched_file" "$_formula_file"
}
start_user_gateway() {
info "restarting openshell-gateway user service as ${TARGET_USER}..."
if ! as_target_user systemctl --user daemon-reload; then
info "could not reach the user systemd manager for ${TARGET_USER}"
info "restart the gateway later with: systemctl --user enable openshell-gateway && systemctl --user restart openshell-gateway"
info "then register it with: openshell gateway add https://127.0.0.1:17670 --local --name openshell"
return 0
fi
as_target_user systemctl --user enable openshell-gateway
as_target_user systemctl --user restart openshell-gateway
as_target_user systemctl --user is-active --quiet openshell-gateway
info "registering local gateway as ${TARGET_USER}..."
register_local_gateway
wait_for_local_gateway_listener
wait_for_local_gateway_status
}
dump_local_gateway_diagnostics() {
_lines="${OPENSHELL_INSTALL_LOG_LINES:-80}"
case "$_lines" in
"" | *[!0-9]*)
_lines=80
;;
esac
info "dumping recent local gateway diagnostics..."
case "${PLATFORM:-}" in
darwin)
dump_homebrew_gateway_diagnostics "$_lines"
;;
linux)
if [ "${LINUX_INSTALL_METHOD:-}" = "snap" ]; then
dump_snap_gateway_diagnostics "$_lines"
else
dump_user_service_gateway_diagnostics "$_lines"
fi
;;
*)
info "no gateway log collector is available for platform: ${PLATFORM:-unknown}"
;;
esac
}
dump_snap_gateway_diagnostics() {
_lines="$1"
info "OpenShell snap service status:"
as_root snap services openshell >&2 || true
info "OpenShell snap connections:"
as_root snap connections openshell >&2 || true
if has_cmd journalctl; then
info "last ${_lines} lines from the OpenShell snap gateway journal:"
as_root journalctl -b -u snap.openshell.gateway.service --no-pager -n "$_lines" >&2 || true
fi
as_root snap logs openshell.gateway -n="$_lines" >&2 || true
}
dump_homebrew_gateway_diagnostics() {
_lines="$1"
_brew_prefix="$(as_target_user brew --prefix 2>/dev/null || true)"
[ -n "$_brew_prefix" ] || _brew_prefix="/opt/homebrew"
info "Homebrew service status:"
as_target_user brew services info "${HOMEBREW_TAP}/${HOMEBREW_FORMULA_NAME}" >&2 || true
for _log_file in \
"${_brew_prefix}/var/log/openshell/openshell-gateway.err.log" \
"${_brew_prefix}/var/log/openshell/openshell-gateway.out.log"; do
if [ -f "$_log_file" ]; then
info "last ${_lines} lines from ${_log_file}:"
tail -n "$_lines" "$_log_file" >&2 || true
else
info "gateway log not found: ${_log_file}"
fi
done
}
dump_user_service_gateway_diagnostics() {
_lines="$1"
if has_cmd systemctl; then
info "openshell-gateway user service status:"
as_target_user systemctl --user status openshell-gateway --no-pager >&2 || true
fi
if has_cmd journalctl; then
info "last ${_lines} lines from openshell-gateway user journal:"
as_target_user journalctl --user -u openshell-gateway --no-pager -n "$_lines" >&2 || true
else
info "journalctl not found; cannot dump openshell-gateway user journal"
fi
}
wait_for_local_gateway_listener() {
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
_elapsed=0
_last_output=""
_probe_url="$(local_gateway_endpoint)/"
_mtls_dir="${TARGET_HOME}/.config/openshell/gateways/openshell/mtls"
info "waiting for local gateway listener to become reachable..."
while [ "$_elapsed" -lt "$_timeout" ]; do
if [ ! -f "${_mtls_dir}/ca.crt" ] || [ ! -f "${_mtls_dir}/tls.crt" ] || [ ! -f "${_mtls_dir}/tls.key" ]; then
_last_output="mTLS client bundle is not ready under ${_mtls_dir}"
elif _last_output="$(as_target_user curl -sS --max-time 2 --cacert "${_mtls_dir}/ca.crt" --cert "${_mtls_dir}/tls.crt" --key "${_mtls_dir}/tls.key" -o /dev/null "$_probe_url" 2>&1)"; then
info "local gateway listener is reachable"
return 0
fi
sleep 1
_elapsed=$((_elapsed + 1))
done
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
dump_local_gateway_diagnostics
error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s"
}
wait_for_local_gateway_status() {
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
_elapsed=0
_status_output=""
_register_bin="${OPENSHELL_REGISTER_BIN:-openshell}"
info "waiting for openshell status to report connected..."
while [ "$_elapsed" -lt "$_timeout" ]; do
if _status_output="$(as_target_user env NO_COLOR=1 "$_register_bin" status 2>&1)"; then
case "$_status_output" in
*"Version:"*)
info "openshell status reports connected"
return 0
;;
esac
fi
sleep 1
_elapsed=$((_elapsed + 1))
done
[ -z "$_status_output" ] || printf '%s\n' "$_status_output" >&2
dump_local_gateway_diagnostics
error "openshell status did not report connected within ${_timeout}s"
}
remove_local_gateway_registration_from() {
_config_dir="$1"
[ -n "$TARGET_HOME" ] || error "cannot resolve home directory for ${TARGET_USER}"
# Replace the CLI registration directly instead of asking `gateway destroy`
# to tear down package-managed resources.
# shellcheck disable=SC2016
as_target_user sh -c '
config_dir=$1
rm -rf "${config_dir}/gateways/local"
mkdir -p "${config_dir}/gateways/openshell"
rm -f \
"${config_dir}/gateways/openshell/metadata.json" \
"${config_dir}/gateways/openshell/edge_token" \
"${config_dir}/gateways/openshell/cf_token" \
"${config_dir}/gateways/openshell/oidc_token.json"
active="${config_dir}/active_gateway"
active_name="$(cat "$active" 2>/dev/null || true)"
if [ "$active_name" = "local" ] || [ "$active_name" = "openshell" ]; then
rm -f "$active"
fi
' sh "$_config_dir"
}
remove_local_gateway_registration() {
remove_local_gateway_registration_from "${TARGET_HOME}/.config/openshell"
}
remove_snap_gateway_registration() {
remove_local_gateway_registration_from \
"${TARGET_HOME}/snap/openshell/common/.config/openshell"
}
register_local_gateway() {
_register_bin="${OPENSHELL_REGISTER_BIN:-openshell}"
_endpoint="$(local_gateway_endpoint)"
if _add_output="$(as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell 2>&1)"; then
[ -z "$_add_output" ] || print_gateway_add_output "$_add_output"
return 0
else
_add_status=$?
fi
case "$_add_output" in
*"already exists"*)
info "local gateway already exists; removing and re-adding it..."
remove_local_gateway_registration
as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell
;;
*)
printf '%s\n' "$_add_output" >&2
return "$_add_status"
;;
esac
}
print_gateway_add_output() {
_endpoint="$(local_gateway_endpoint)"
printf '%s\n' "$1" | while IFS= read -r _line; do
case "$_line" in
*"Gateway is not reachable at ${_endpoint}"*) ;;
*"Verify the gateway is running and the endpoint is correct."*) ;;
*) printf '%s\n' "$_line" >&2 ;;
esac
done
}
install_linux_deb() {
check_linux_deb_platform
set_linux_target_runtime_dir
_arch="$(get_deb_arch)"
_tmpdir="$(mktemp -d)"
chmod 0755 "$_tmpdir"
trap 'rm -rf "$_tmpdir"' EXIT
prepare_prerelease_assets "$_tmpdir"
info "downloading ${RELEASE_TAG} release checksums..."
download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "${_tmpdir}/${CHECKSUMS_NAME}" || {
error "failed to download ${CHECKSUMS_NAME} for ${RELEASE_TAG}"
}
_deb_file="$(find_deb_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch")"
if [ -z "$_deb_file" ]; then
error "no Debian package found for architecture: ${_arch}"
fi
_deb_path="${_tmpdir}/${_deb_file}"
info "selected ${_deb_file}"
info "downloading ${_deb_file}..."
download_release_asset "$RELEASE_TAG" "$_deb_file" "$_deb_path" || {
error "failed to download ${_deb_file} for ${RELEASE_TAG}"
}
chmod 0644 "$_deb_path"
info "verifying checksum..."
verify_checksum "$_deb_path" "${_tmpdir}/${CHECKSUMS_NAME}" "$_deb_file"
info "installing ${_deb_file}..."
install_deb_package "$_deb_path"
info "installed ${APP_NAME} package from ${RELEASE_TAG}"
start_user_gateway
}
install_linux_rpm() {
require_cmd rpm
set_linux_target_runtime_dir
_arch="$(get_rpm_arch)"
_tmpdir="$(mktemp -d)"
chmod 0755 "$_tmpdir"
trap 'rm -rf "$_tmpdir"' EXIT
prepare_prerelease_assets "$_tmpdir"
info "downloading ${RELEASE_TAG} release checksums..."
download_release_asset "$RELEASE_TAG" "$CHECKSUMS_NAME" "${_tmpdir}/${CHECKSUMS_NAME}" || {
error "failed to download ${CHECKSUMS_NAME} for ${RELEASE_TAG}"
}
_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell)"
if [ -z "$_rpm_file" ]; then
error "no openshell RPM package found for architecture: ${_arch}"
fi
_gateway_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell-gateway)"
if [ -z "$_gateway_rpm_file" ]; then
error "no openshell-gateway RPM package found for architecture: ${_arch}"
fi
_prover_rpm_file="$(find_rpm_asset "${_tmpdir}/${CHECKSUMS_NAME}" "$_arch" openshell-prover)"
if [ -z "$_prover_rpm_file" ]; then
error "no openshell-prover RPM package found for architecture: ${_arch}"
fi
info "selected ${_rpm_file}, ${_gateway_rpm_file}, and ${_prover_rpm_file}"
for _package_file in "$_rpm_file" "$_gateway_rpm_file" "$_prover_rpm_file"; do
_package_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/${_package_file}"
_package_path="${_tmpdir}/${_package_file}"
info "downloading ${_package_file}..."
download_release_asset "$RELEASE_TAG" "$_package_file" "$_package_path" || {
error "failed to download ${_package_file} for ${RELEASE_TAG}"
}
chmod 0644 "$_package_path"
info "verifying checksum for ${_package_file}..."
verify_checksum "$_package_path" "${_tmpdir}/${CHECKSUMS_NAME}" "$_package_file"
done
info "installing ${_rpm_file}, ${_gateway_rpm_file}, and ${_prover_rpm_file}..."
install_rpm_packages \
"${_tmpdir}/${_rpm_file}" \
"${_tmpdir}/${_gateway_rpm_file}" \
"${_tmpdir}/${_prover_rpm_file}"
info "installed ${APP_NAME} RPM packages from ${RELEASE_TAG}"
start_user_gateway
}
openshell_snap_channel() {
case "${OPENSHELL_VERSION:-}" in
dev) printf '%s\n' "latest/edge" ;;
'') printf '%s\n' "latest/stable" ;;
*) error "Snap installs do not support OPENSHELL_VERSION=${OPENSHELL_VERSION}; use a native package" ;;
esac
}
wait_for_docker_daemon() {
_timeout="${OPENSHELL_INSTALL_DOCKER_TIMEOUT:-30}"
_elapsed=0
_last_output=""
info "waiting for Docker daemon to become reachable..."
while [ "$_elapsed" -lt "$_timeout" ]; do
if _last_output="$(as_root docker info 2>&1)"; then
info "Docker daemon is reachable"
return 0
fi
sleep 1
_elapsed=$((_elapsed + 1))
done
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
if snap list docker >/dev/null 2>&1; then
as_root snap services docker >&2 || true
as_root snap changes >&2 || true
fi
error "Docker daemon did not become reachable within ${_timeout}s"
}
# Copy the snap gateway's client bundle into the target user's snap state
# directory, where `openshell gateway add --local` imports it. Root only reads
# the source files; the target user writes the copies into their own home.
copy_snap_client_bundle() {
_src="${OPENSHELL_SNAP_TLS_DIR:-/var/snap/openshell/common/tls}"
_dst="${TARGET_HOME}/snap/openshell/common/.local/state/openshell/tls"
as_target_user mkdir -p "${_dst}/client"
as_target_user chmod 700 "$_dst" "${_dst}/client"
for _file in ca.crt client/tls.crt client/tls.key; do
as_root cat "${_src}/${_file}" |
as_target_user sh -c 'umask 077; cat >"$1"' sh "${_dst}/${_file}"
as_target_user chmod 600 "${_dst}/${_file}"
done
}
# Snap revisions that require mTLS ship the post-refresh hook that migrates
# older plaintext configs.
snap_gateway_uses_mtls() {
[ -e "${OPENSHELL_SNAP_DIR:-/snap/openshell/current}/meta/hooks/post-refresh" ]
}
register_snap_gateway() {
_register_bin="${OPENSHELL_REGISTER_BIN:-/snap/bin/openshell}"
if snap_gateway_uses_mtls; then
_endpoint="https://127.0.0.1:${LOCAL_GATEWAY_PORT}"
info "copying the gateway client certificate for ${TARGET_USER}..."
copy_snap_client_bundle
else
_endpoint="http://127.0.0.1:${LOCAL_GATEWAY_PORT}"
warn "this OpenShell snap revision serves plaintext HTTP without client authentication; any local user can operate the gateway"
fi
if _add_output="$(as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell 2>&1)"; then
[ -z "$_add_output" ] || print_gateway_add_output "$_add_output"
return 0
else
_add_status=$?
fi
case "$_add_output" in
*"already exists"*)
info "local gateway already exists; removing and re-adding it..."
remove_snap_gateway_registration
as_target_user "$_register_bin" gateway add "$_endpoint" --local --name openshell
;;
*)
printf '%s\n' "$_add_output" >&2
return "$_add_status"
;;
esac
}
# The mTLS gateway rejects TLS handshakes without a client certificate, so
# probe it with the root-owned client bundle.
wait_for_snap_gateway_listener() {
_timeout="${OPENSHELL_INSTALL_GATEWAY_TIMEOUT:-30}"
_elapsed=0
_last_output=""
_tls_dir="${OPENSHELL_SNAP_TLS_DIR:-/var/snap/openshell/common/tls}"
if snap_gateway_uses_mtls; then
_probe_url="https://127.0.0.1:${LOCAL_GATEWAY_PORT}/"
_probe_as=as_root
set -- --cacert "${_tls_dir}/ca.crt" \
--cert "${_tls_dir}/client/tls.crt" --key "${_tls_dir}/client/tls.key"
else
_probe_url="http://127.0.0.1:${LOCAL_GATEWAY_PORT}/"
_probe_as=""
set --
fi
info "waiting for local gateway listener to become reachable..."
while [ "$_elapsed" -lt "$_timeout" ]; do
if _last_output="$($_probe_as curl -sS --max-time 2 "$@" -o /dev/null "$_probe_url" 2>&1)"; then
info "local gateway listener is reachable"
return 0
fi
sleep 1
_elapsed=$((_elapsed + 1))
done
[ -z "$_last_output" ] || printf '%s\n' "$_last_output" >&2
dump_local_gateway_diagnostics
error "local gateway listener did not become reachable at ${_probe_url} within ${_timeout}s"
}
install_linux_snap() {
require_cmd snap
set_linux_target_runtime_dir
if snap list docker >/dev/null 2>&1; then
error "the Docker snap is not currently compatible with OpenShell because its AppArmor confinement prevents OpenShell's hardened containers from starting.
Remove the Docker snap and install Docker Engine from a system package or Docker's package repository, then rerun this installer."
fi
if ! has_cmd docker; then
error "Docker is required before installing the OpenShell snap.
Install Docker Engine from a system package or Docker's package repository, then rerun this installer. The Docker snap is not currently compatible with OpenShell."
fi
info "using existing Docker installation"
wait_for_docker_daemon
_channel="$(openshell_snap_channel)"
if snap list openshell >/dev/null 2>&1; then
info "refreshing OpenShell snap from ${_channel}..."
as_root snap refresh openshell --channel="$_channel"
warn "restarting the OpenShell gateway to use the refreshed snap; active sandbox sessions will be interrupted"
else
info "installing OpenShell snap from ${_channel}..."
as_root snap install openshell --channel="$_channel"
fi
as_root snap restart openshell.gateway
info "installed OpenShell snap from ${_channel}"
wait_for_snap_gateway_listener
info "registering local gateway as ${TARGET_USER}..."
register_snap_gateway
OPENSHELL_REGISTER_BIN="/snap/bin/openshell"
wait_for_local_gateway_status
}
install_macos_homebrew() {
check_macos_platform
_tmpdir="$(mktemp -d)"
chmod 0755 "$_tmpdir"
trap 'rm -rf "$_tmpdir"' EXIT
prepare_prerelease_assets "$_tmpdir"
_formula_file="${_tmpdir}/openshell.rb"
_formula_url="${GITHUB_URL}/releases/download/${RELEASE_TAG}/openshell.rb"
info "downloading Homebrew formula from ${_formula_url}..."
download_release_asset "$RELEASE_TAG" "openshell.rb" "$_formula_file" || {
error "failed to download ${_formula_url}; the selected release may not include a Homebrew formula"
}
chmod 0644 "$_formula_file"
patch_prerelease_homebrew_formula_urls "$_formula_file"
patch_homebrew_formula "$_formula_file"
_tap_formula_file="$(homebrew_formula_path "$HOMEBREW_TAP" "$HOMEBREW_FORMULA_NAME")"
info "staging Homebrew formula in tap ${HOMEBREW_TAP}..."
cp "$_formula_file" "$_tap_formula_file"
chmod 0644 "$_tap_formula_file"
if [ "$(id -u)" -eq 0 ]; then
chown "$TARGET_USER" "$_tap_formula_file" 2>/dev/null || true
fi
_formula_ref="${HOMEBREW_TAP}/${HOMEBREW_FORMULA_NAME}"
if as_target_user brew list --formula openshell >/dev/null 2>&1; then
info "reinstalling OpenShell with Homebrew..."
as_target_user brew reinstall --formula "$_formula_ref"
else
info "installing OpenShell with Homebrew..."
as_target_user brew install --formula "$_formula_ref"
fi
info "restarting OpenShell Homebrew service..."
if ! as_target_user brew services restart "$_formula_ref"; then
warn "could not restart the OpenShell Homebrew service"
info "restart it later with: brew services restart ${_formula_ref}"
info "then register it with: openshell gateway add $(local_gateway_endpoint) --local --name openshell"
return 0
fi
_brew_prefix="$(as_target_user brew --prefix 2>/dev/null || true)"
if [ -n "$_brew_prefix" ] && [ -x "${_brew_prefix}/bin/openshell" ]; then
OPENSHELL_REGISTER_BIN="${_brew_prefix}/bin/openshell"
fi
info "registering local gateway as ${TARGET_USER}..."
register_local_gateway
wait_for_local_gateway_listener
wait_for_local_gateway_status
}
main() {
if [ "$#" -gt 0 ]; then
case "$1" in
--help)
usage
exit 0
;;
*)
error "unknown option: $1"
;;
esac
fi
require_cmd curl
PLATFORM="$(detect_platform)"
if [ "$PLATFORM" = "linux" ]; then
LINUX_INSTALL_METHOD="$(linux_package_method)"
fi
TARGET_USER="$(target_user)"
TARGET_UID="$(id -u "$TARGET_USER" 2>/dev/null || true)"
[ -n "$TARGET_UID" ] || error "cannot resolve uid for ${TARGET_USER}"
TARGET_HOME="$(user_home "$TARGET_USER")"
if [ "${LINUX_INSTALL_METHOD:-}" = "snap" ]; then
guard_native_to_snap_transition
else
RELEASE_TAG="$(resolve_release_tag)"
guard_breaking_upgrade
fi
case "$PLATFORM" in
linux)
case "$LINUX_INSTALL_METHOD" in
snap)
install_linux_snap
;;
deb)
require_linux_package_glibc
install_linux_deb
;;
rpm)
require_linux_package_glibc
install_linux_rpm
;;
*)
error "unsupported Linux package method"
;;
esac
;;
darwin)
install_macos_homebrew
;;
*)
error "unsupported platform: ${PLATFORM}"
;;
esac
}
if [ "${OPENSHELL_INSTALL_SH_TEST:-0}" != "1" ]; then
main "$@"
fi