diff --git a/deploy/snap/README.md b/deploy/snap/README.md deleted file mode 100644 index 419aacaa4..000000000 --- a/deploy/snap/README.md +++ /dev/null @@ -1,177 +0,0 @@ -# Building a snap package - -OpenShell snap packages are defined by the root `snapcraft.yaml` and built with -Snapcraft from source. - -The helper task under `tasks/` still stages the same payload from pre-built -binaries when you want to inspect the snap root or produce local artifacts. - -## Prerequisites - -- Linux on `amd64` or `arm64` -- `snap` from `snapd` -- `snapcraft` -- Docker from the Docker snap (`sudo snap install docker`) - -## Build with Snapcraft - -Build the snap from source with the root manifest: - -```shell -snapcraft pack -``` - -The manifest builds the Rust binaries inside Snapcraft, installs the CLI, -gateway, and sandbox supervisor into the snap, and keeps the same runtime -environment as the current deployment logic. - -## Staged helper flow - -The helper task under `tasks/` still stages the same payload from pre-built -binaries when you want to inspect the snap root or produce local artifacts. - -For that flow, install `mise` and build: - -- `openshell` -- `openshell-gateway` -- `openshell-sandbox` - -## Build helper binaries - -Build the release binaries used by the staged helper flow: - -```shell -mise run build:rust:snap -``` - -This convenience target builds the CLI with `bundled-z3`, the gateway, and -`openshell-sandbox` for the Docker driver to bind-mount into sandbox containers. - -## Pack the snap - -Run the packaging hook through mise: - -```shell -VERSION="$(uv run python tasks/scripts/release.py get-version --snap)" - -OPENSHELL_CLI_BINARY="$PWD/target/release/openshell" \ -OPENSHELL_GATEWAY_BINARY="$PWD/target/release/openshell-gateway" \ -OPENSHELL_DOCKER_SUPERVISOR_BINARY="$PWD/target/release/openshell-sandbox" \ -OPENSHELL_SNAP_VERSION="$VERSION" \ -OPENSHELL_OUTPUT_DIR=artifacts \ - mise run package:snap -``` - -The artifact is written to `artifacts/openshell_${VERSION}_${ARCH}.snap`. The -packaging hook fails before `snap pack` if `openshell-sandbox` is missing or not -executable. - -## Stage without packing - -To inspect the snap root without running `snap pack`: - -```shell -VERSION="$(uv run python tasks/scripts/release.py get-version --snap)" - -OPENSHELL_CLI_BINARY="$PWD/target/release/openshell" \ -OPENSHELL_GATEWAY_BINARY="$PWD/target/release/openshell-gateway" \ -OPENSHELL_DOCKER_SUPERVISOR_BINARY="$PWD/target/release/openshell-sandbox" \ -OPENSHELL_SNAP_VERSION="$VERSION" \ - mise run package:snap:stage -``` - -The staged root is written to `artifacts/snap-root`. - -## Commands in the snap - -The snap exposes the CLI: - -- `openshell` - -It also defines a system service with packaged Docker driver settings. - -- `openshell.gateway` - -The gateway service uses `refresh-mode: endure` so snap refreshes do not restart -it while sandboxes are active. Restart the service manually when you are ready -to move the gateway to the refreshed snap revision. - -`openshell-sandbox` is staged next to `openshell-gateway` as the Docker -supervisor binary. The gateway app starts through a small wrapper that sets -Snap-specific defaults and reads `$SNAP_COMMON/gateway.toml` when that file -exists. The service stores its gateway database under `$SNAP_COMMON`. - -## Interfaces - -The `openshell` CLI app plugs: - -- `home` -- `network` -- `ssh-keys` -- `system-observe` - -The `openshell.gateway` service plugs: - -- `docker` -- `log-observe` -- `network` -- `network-bind` -- `ssh-keys` -- `system-observe` - -## Start a Docker gateway from the snap - -The snapped gateway talks to Docker through the Docker snap's -`docker:docker-daemon` slot. The snap declares `default-provider: docker` on -its Docker plug so snapd can install the Docker snap when OpenShell is -installed. Connect the interface before using the Docker driver: - -```shell -sudo snap connect openshell:docker docker:docker-daemon -sudo snap connect openshell:log-observe -sudo snap connect openshell:system-observe -sudo snap connect openshell:ssh-keys -``` - -The gateway uses Docker's default Unix socket location. The Docker snap exposes -that socket through the connected `docker` interface, so no `DOCKER_HOST` -override is required. The OpenShell snap still requires the Docker snap because -it relies on the `docker:docker-daemon` slot; it does not work with Docker -installed from a Debian package or Docker's upstream packages. - -The service runs the gateway with Snap-specific environment defaults: - -```shell -OPENSHELL_DISABLE_TLS=true \ -OPENSHELL_DB_URL="sqlite:$SNAP_COMMON/gateway.db?mode=rwc" \ -openshell.gateway -``` - -This stores the gateway SQLite database at -`/var/snap/openshell/common/gateway.db`. Create -`/var/snap/openshell/common/gateway.toml` when you need to override gateway or -Docker driver settings. - -## Connect with the OpenShell CLI - -Register the snap-run gateway as a local plaintext gateway: - -```shell -openshell gateway add http://127.0.0.1:17670 --local --name snap-docker -openshell gateway select snap-docker -openshell status -``` - -Then use normal sandbox commands: - -```shell -openshell sandbox create --name demo -openshell sandbox connect demo -``` - -To avoid changing the default gateway, pass the gateway name per command: - -```shell -openshell --gateway snap-docker status -openshell --gateway snap-docker sandbox create --name demo -``` diff --git a/deploy/snap/meta/snap.yaml.in b/deploy/snap/meta/snap.yaml.in deleted file mode 100644 index 920dd9141..000000000 --- a/deploy/snap/meta/snap.yaml.in +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -name: openshell -title: OpenShell -version: "@VERSION@" -summary: Safe, sandboxed runtimes for autonomous AI agents -description: | - OpenShell provides safe, sandboxed runtimes for autonomous AI agents. - It offers a CLI for managing gateways, sandboxes, and providers with - policy-enforced egress routing, credential proxying, and privacy-aware - LLM inference routing. - -base: "@BASE@" -grade: "@GRADE@" -confinement: strict -license: Apache-2.0 -website: https://docs.nvidia.com/openshell/latest/index.html -source-code: https://github.com/NVIDIA/OpenShell -issues: https://github.com/NVIDIA/OpenShell/issues -contact: https://github.com/NVIDIA/OpenShell/security/policy -architectures: - - "@ARCH@" - -apps: - openshell: - command: bin/openshell - plugs: - - home - - network - - ssh-keys - - system-observe - gateway: - command: bin/openshell-gateway-wrapper - daemon: simple - refresh-mode: endure - environment: - XDG_DATA_HOME: "$SNAP_COMMON" - # Used for creating and locating certain sockets. - XDG_RUNTIME_DIR: "$SNAP_COMMON" - - plugs: - - docker - - log-observe - - network - - network-bind - - ssh-keys - - system-observe - -plugs: - docker: - interface: docker - default-provider: docker diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index cd9973f13..0949c44e0 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -75,6 +75,65 @@ To keep the user service running after logout, enable linger: sudo loginctl enable-linger $USER ``` +## Snap + +Install the OpenShell snap from the Snap Store: + +```shell +sudo snap install openshell --classic +``` + +The snap defines two apps: the `openshell` CLI and the `openshell.gateway` +systemd-style service. The gateway listens on `https://127.0.0.1:17670` and +stores its database at `$SNAP_COMMON/gateway.db` (typically +`/var/snap/openshell/common/gateway.db`). Create `$SNAP_COMMON/gateway.toml` +when you need to override gateway settings. + +### Snap store installs + +When installing from the Snap Store, snapd automatically connects the `home`, +`network`, `network-bind`, and `ssh-keys` plugs. The `docker` plug still +requires manual connection: + +```shell +sudo snap connect openshell:docker docker:docker-daemon +``` + +The snap declares `default-provider: docker` on the Docker plug so snapd will +offer to install the Docker snap, but the connection itself must be made +manually. + +### Locally built snap packages + +When installing a locally built `.snap` file, no plugs are connected by default: + +```shell +sudo snap install ./openshell_*.snap --dangerous --classic +sudo snap connect openshell:home +sudo snap connect openshell:network +sudo snap connect openshell:network-bind +sudo snap connect openshell:ssh-keys +sudo snap connect openshell:docker docker:docker-daemon +sudo snap connect openshell:log-observe +sudo snap connect openshell:system-observe +``` + +The `log-observe` and `system-observe` plugs are needed for the gateway service +to read logs and inspect system processes. The `docker` plug requires the +`docker:docker-daemon` slot from the Docker snap and does not work with +system-installed Docker. + +### Gateway service + +The gateway runs as a snap daemon with `refresh-mode: endure`, meaning snapd +will not restart it during snap refreshes. This prevents the gateway from +killing active sandbox sessions mid-refresh. Restart the service manually after +a snap refresh when you need the updated binary: + +```shell +sudo systemctl restart snap.openshell.gateway +``` + ## Kubernetes Kubernetes deployments use the OpenShell Helm chart. For step-by-step installation, refer to [Kubernetes Setup](/kubernetes/setup). For chart values and packaging details, refer to the [Helm chart README](https://github.com/NVIDIA/OpenShell/blob/main/deploy/helm/openshell/README.md). diff --git a/python/openshell/release_formula_test.py b/python/openshell/release_formula_test.py index 81bf89fab..47abfc2a9 100644 --- a/python/openshell/release_formula_test.py +++ b/python/openshell/release_formula_test.py @@ -96,7 +96,7 @@ def test_generate_homebrew_formula_uses_tagged_macos_driver_asset_without_defaul def test_snap_wrapper_uses_optional_gateway_config_without_generating_toml() -> None: repo_root = Path(__file__).resolve().parents[2] - wrapper = (repo_root / "deploy/snap/bin/openshell-gateway-wrapper").read_text( + wrapper = (repo_root / "tasks/scripts/snap-gateway-wrapper.sh").read_text( encoding="utf-8" ) diff --git a/snapcraft.yaml b/snapcraft.yaml index f17dd8a58..ce0ca3f76 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -38,12 +38,21 @@ apps: gateway: command: bin/openshell-gateway-wrapper daemon: simple + # refresh-mode: endure prevents snapd from restarting the gateway daemon + # during snap refreshes, which would kill active sandbox sessions. + # Operators must manually restart the service after a refresh if needed. refresh-mode: endure + # The wrapper sets OPENSHELL_DISABLE_TLS=true and OPENSHELL_DB_URL to + # use $SNAP_COMMON/gateway.db. If $SNAP_COMMON/gateway.toml exists it is + # passed to the gateway as --config, allowing operators to override + # settings without rebuilding the snap. environment: XDG_DATA_HOME: "$SNAP_COMMON" XDG_RUNTIME_DIR: "$SNAP_COMMON" plugs: - docker + # Docker snap is required because the snap uses the docker:docker-daemon + # interface slot. It does not work with system-installed Docker. - log-observe - network - network-bind @@ -82,7 +91,7 @@ parts: "$CRAFT_PART_INSTALL/bin/openshell-gateway" install -D -m 0755 "$CRAFT_PART_BUILD/target/release/openshell-sandbox" \ "$CRAFT_PART_INSTALL/bin/openshell-sandbox" - install -D -m 0755 "$CRAFT_PROJECT_DIR/deploy/snap/bin/openshell-gateway-wrapper" \ + install -D -m 0755 "$CRAFT_PROJECT_DIR/tasks/scripts/snap-gateway-wrapper.sh" \ "$CRAFT_PART_INSTALL/bin/openshell-gateway-wrapper" install -D -m 0644 "$CRAFT_PROJECT_DIR/LICENSE" \ "$CRAFT_PART_INSTALL/usr/share/doc/openshell/LICENSE" diff --git a/tasks/package.toml b/tasks/package.toml index 08436a632..496f93de2 100644 --- a/tasks/package.toml +++ b/tasks/package.toml @@ -23,26 +23,3 @@ hide = true ["package:deb:install"] description = "Build OpenShell from source and install the deb locally (requires sudo)" run = "tasks/scripts/package-deb-install.sh" - -["package:snap"] -description = "Build a snap package from supplied OpenShell binaries" -run = "tasks/scripts/package-snap.sh" -hide = true - -["package:snap:amd64"] -description = "Build an amd64 snap package from supplied OpenShell binaries" -env = { OPENSHELL_SNAP_ARCH = "amd64" } -run = "tasks/scripts/package-snap.sh" -hide = true - -["package:snap:arm64"] -description = "Build an arm64 snap package from supplied OpenShell binaries" -env = { OPENSHELL_SNAP_ARCH = "arm64" } -run = "tasks/scripts/package-snap.sh" -hide = true - -["package:snap:stage"] -description = "Stage a snap root from supplied OpenShell binaries without running snap pack" -env = { OPENSHELL_SNAP_PACK = "0", OPENSHELL_SNAP_STAGE_DIR = "artifacts/snap-root" } -run = "tasks/scripts/package-snap.sh" -hide = true diff --git a/tasks/scripts/package-snap.sh b/tasks/scripts/package-snap.sh deleted file mode 100755 index c7f26e0ce..000000000 --- a/tasks/scripts/package-snap.sh +++ /dev/null @@ -1,219 +0,0 @@ -#!/usr/bin/env bash -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -# -# Stage a hand-authored OpenShell snap payload from pre-built binaries, then -# optionally invoke `snap pack` against the staged directory. - -set -euo pipefail - -APP_NAME="openshell" - -usage() { - cat <<'EOF' -Build or stage the openshell snap from supplied binaries. - -Required environment: - OPENSHELL_CLI_BINARY Path to openshell - OPENSHELL_GATEWAY_BINARY Path to openshell-gateway - OPENSHELL_DOCKER_SUPERVISOR_BINARY - Path to the Linux openshell-sandbox supervisor - OPENSHELL_SNAP_VERSION Snap version - -Optional environment: - OPENSHELL_SNAP_ARCH Snap architecture (amd64 or arm64; defaults to host arch) - OPENSHELL_SNAP_BASE Snap base (default: core24) - OPENSHELL_SNAP_GRADE Snap grade (default: devel) - OPENSHELL_SNAP_PACK Set to 0 to only stage the snap root (default: 1) - OPENSHELL_SNAP_STAGE_DIR Directory to stage into (default: temporary directory) - OPENSHELL_OUTPUT_DIR Output directory for .snap artifacts (default: artifacts) -EOF -} - -require_env() { - local name="$1" - if [ -z "${!name:-}" ]; then - echo "error: ${name} is required" >&2 - usage >&2 - exit 2 - fi -} - -stage_binary() { - local src="$1" - local dst="$2" - if [ ! -x "$src" ]; then - echo "error: binary is missing or not executable: ${src}" >&2 - exit 1 - fi - mkdir -p "$(dirname "$dst")" - install -m 0755 "$src" "$dst" -} - -infer_snap_arch() { - case "$(uname -m)" in - x86_64 | amd64) echo "amd64" ;; - aarch64 | arm64) echo "arm64" ;; - *) uname -m ;; - esac -} - -normalize_bool() { - local val - val="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" - case "${val}" in - 1 | true | yes | on) echo "1" ;; - 0 | false | no | off) echo "0" ;; - *) - echo "error: invalid boolean value '${1}' (expected true/false, 1/0, yes/no, on/off)" >&2 - exit 2 - ;; - esac -} - -prepare_stage_dir() { - local dir="$1" - - if [ -z "$dir" ]; then - echo "error: refusing empty OPENSHELL_SNAP_STAGE_DIR" >&2 - exit 2 - fi - - mkdir -p "$dir" - - local canonical_dir - local canonical_repo_root - canonical_dir="$(cd "$dir" && pwd -P)" - canonical_repo_root="$(cd "$repo_root" && pwd -P)" - - if [[ "$canonical_dir" == "/" || - "$canonical_dir" == "$canonical_repo_root" || - "$canonical_repo_root" == "$canonical_dir/"* ]]; then - echo "error: refusing unsafe OPENSHELL_SNAP_STAGE_DIR: '${dir}' resolved to '${canonical_dir}'" >&2 - exit 2 - fi - - find "$canonical_dir" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} + -} - -render_snap_yaml() { - local template="$1" - local output="$2" - - awk \ - -v version="$OPENSHELL_SNAP_VERSION" \ - -v base="$OPENSHELL_SNAP_BASE" \ - -v grade="$OPENSHELL_SNAP_GRADE" \ - -v arch="$OPENSHELL_SNAP_ARCH" ' - { - gsub(/@VERSION@/, version); - gsub(/@BASE@/, base); - gsub(/@GRADE@/, grade); - gsub(/@ARCH@/, arch); - print; - } - ' "$template" >"$output" -} - -# --------------------------------------------------------------------------- -# Inputs -# --------------------------------------------------------------------------- - -require_env OPENSHELL_CLI_BINARY -require_env OPENSHELL_GATEWAY_BINARY -require_env OPENSHELL_DOCKER_SUPERVISOR_BINARY -require_env OPENSHELL_SNAP_VERSION - -OPENSHELL_SNAP_ARCH="${OPENSHELL_SNAP_ARCH:-$(infer_snap_arch)}" -OPENSHELL_SNAP_BASE="${OPENSHELL_SNAP_BASE:-core24}" -OPENSHELL_SNAP_GRADE="${OPENSHELL_SNAP_GRADE:-devel}" -OPENSHELL_SNAP_PACK="$(normalize_bool "${OPENSHELL_SNAP_PACK:-1}")" - -case "$OPENSHELL_SNAP_ARCH" in -amd64 | arm64) ;; -*) - echo "error: OPENSHELL_SNAP_ARCH must be amd64 or arm64, got ${OPENSHELL_SNAP_ARCH}" >&2 - exit 2 - ;; -esac - -case "$OPENSHELL_SNAP_GRADE" in -devel | stable) ;; -*) - echo "error: OPENSHELL_SNAP_GRADE must be devel or stable, got ${OPENSHELL_SNAP_GRADE}" >&2 - exit 2 - ;; -esac - -repo_root="$(cd "$(dirname "$0")/../.." && pwd)" -src_dir="${repo_root}/deploy/snap" -template="${src_dir}/meta/snap.yaml.in" -output_dir_input="${OPENSHELL_OUTPUT_DIR:-artifacts}" -case "$output_dir_input" in -/*) output_dir="$output_dir_input" ;; -*) output_dir="${repo_root}/${output_dir_input}" ;; -esac -mkdir -p "$output_dir" - -if [ ! -f "$template" ]; then - echo "error: snap metadata template not found: ${template}" >&2 - exit 1 -fi - -tmpdir="$(mktemp -d)" -cleanup() { - rm -rf "$tmpdir" -} -trap cleanup EXIT - -if [ -n "${OPENSHELL_SNAP_STAGE_DIR:-}" ]; then - case "$OPENSHELL_SNAP_STAGE_DIR" in - /*) snap_root="$OPENSHELL_SNAP_STAGE_DIR" ;; - *) snap_root="${repo_root}/${OPENSHELL_SNAP_STAGE_DIR}" ;; - esac - prepare_stage_dir "$snap_root" -else - snap_root="${tmpdir}/snap-root" - mkdir -p "$snap_root" -fi - -# --------------------------------------------------------------------------- -# Stage the snap payload -# --------------------------------------------------------------------------- - -stage_binary "$OPENSHELL_CLI_BINARY" "$snap_root/bin/openshell" -stage_binary "$OPENSHELL_GATEWAY_BINARY" "$snap_root/bin/openshell-gateway" -stage_binary "$OPENSHELL_DOCKER_SUPERVISOR_BINARY" "$snap_root/bin/openshell-sandbox" -install -D -m 0755 "${repo_root}/deploy/snap/bin/openshell-gateway-wrapper" \ - "$snap_root/bin/openshell-gateway-wrapper" - -install -D -m 0644 "${repo_root}/LICENSE" "$snap_root/usr/share/doc/openshell/LICENSE" -install -D -m 0644 "${repo_root}/README.md" "$snap_root/usr/share/doc/openshell/README.md" - -mkdir -p "$snap_root/meta" -render_snap_yaml "$template" "$snap_root/meta/snap.yaml" - -# --------------------------------------------------------------------------- -# Smoke tests -# --------------------------------------------------------------------------- - -"$snap_root/bin/openshell" --version -"$snap_root/bin/openshell-gateway" --version -"$snap_root/bin/openshell-sandbox" --version - -# --------------------------------------------------------------------------- -# Pack -# --------------------------------------------------------------------------- - -if [ "$OPENSHELL_SNAP_PACK" = "0" ]; then - echo "Staged snap root at ${snap_root}" - exit 0 -fi - -if ! command -v snap >/dev/null 2>&1; then - echo "error: snap command not found; install snapd or set OPENSHELL_SNAP_PACK=0 to only stage the snap root" >&2 - exit 1 -fi - -snap pack "$snap_root" "$output_dir" -echo "Wrote ${output_dir}/${APP_NAME}_${OPENSHELL_SNAP_VERSION}_${OPENSHELL_SNAP_ARCH}.snap" diff --git a/deploy/snap/bin/openshell-gateway-wrapper b/tasks/scripts/snap-gateway-wrapper.sh similarity index 65% rename from deploy/snap/bin/openshell-gateway-wrapper rename to tasks/scripts/snap-gateway-wrapper.sh index cfba8db36..14d2054f7 100755 --- a/deploy/snap/bin/openshell-gateway-wrapper +++ b/tasks/scripts/snap-gateway-wrapper.sh @@ -2,6 +2,12 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# Snap wrapper for openshell-gateway. Sets snap-specific defaults: +# - OPENSHELL_DB_URL -> sqlite:$SNAP_COMMON/gateway.db (overridable) +# - OPENSHELL_DISABLE_TLS -> true +# If $SNAP_COMMON/gateway.toml exists, passes it as --config so operators +# can override settings without rebuilding the snap. + set -eu CANONICAL_CONFIG_FILE="${SNAP_COMMON}/gateway.toml"