diff --git a/.packit.yaml b/.packit.yaml index 3a608111b..5d1f65063 100644 --- a/.packit.yaml +++ b/.packit.yaml @@ -35,12 +35,10 @@ actions: - 'bash -c "echo openshell-${PACKIT_PROJECT_VERSION}.tar.gz"' fix-spec-file: - # Update Source0 to the generated tarball name - - 'bash -c "sed -i \"s|^Source0:.*|Source0: openshell-${PACKIT_PROJECT_VERSION}.tar.gz|\" openshell.spec"' - # Update Source1 to the generated vendor tarball name - - 'bash -c "sed -i \"s|^Source1:.*|Source1: openshell-${PACKIT_PROJECT_VERSION}-vendor.tar.xz|\" openshell.spec"' - # Update Version - - 'bash -c "sed -i -r \"s/^Version:(\\s*)\\S+/Version:\\1${PACKIT_RPMSPEC_VERSION}/\" openshell.spec"' + # Update the canonical version macro. Version:, Source0:, Source1:, and all + # other version references expand from %{openshell_version} so only this + # one line needs updating. + - 'bash -c "sed -i -r \"s/^%global openshell_version .*/%global openshell_version ${PACKIT_RPMSPEC_VERSION}/\" openshell.spec"' # Update Release - 'bash -c "RELEASE=${OPENSHELL_RPM_RELEASE:-${PACKIT_RPMSPEC_RELEASE}} && sed -i -r \"s/^Release:(\\s*)\\S+/Release:\\1${RELEASE}%{?dist}/\" openshell.spec"' # Keep embedded binary metadata aligned with the release workflow. Python diff --git a/crates/openshell-server/src/config_file.rs b/crates/openshell-server/src/config_file.rs index 2a1320a55..db0dcd684 100644 --- a/crates/openshell-server/src/config_file.rs +++ b/crates/openshell-server/src/config_file.rs @@ -515,4 +515,47 @@ version = 2 .expect_err("missing file must be io error"); assert!(matches!(err, ConfigFileError::Io { .. })); } + + /// Contract test: the RPM default config template must parse against the + /// current schema and must pin the settings that Podman deployments require. + /// + /// This test loads `deploy/rpm/gateway.toml.default` through the same + /// `load()` path that the gateway uses at runtime, catching: + /// - template corruption or unknown fields (`deny_unknown_fields`) + /// - schema drift (version bump or field renames) + /// - accidental changes to the bind address or compute driver list + #[test] + fn rpm_default_config_parses_and_has_podman_defaults() { + let path = + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../deploy/rpm/gateway.toml.default"); + let config = + load(&path).expect("deploy/rpm/gateway.toml.default must parse against current schema"); + let gw = &config.openshell.gateway; + + let addr = gw + .bind_address + .expect("bind_address must be explicitly set in the RPM default config"); + assert!( + addr.ip().is_unspecified(), + "RPM default bind_address must be 0.0.0.0 so Podman sandbox containers \ + can reach the gateway over the host network bridge, got {addr}" + ); + assert_eq!( + addr.port(), + openshell_core::config::DEFAULT_SERVER_PORT, + "RPM default port must match DEFAULT_SERVER_PORT ({})", + openshell_core::config::DEFAULT_SERVER_PORT + ); + + let drivers = gw + .compute_drivers + .as_ref() + .expect("compute_drivers must be explicitly set in the RPM default config"); + assert_eq!( + drivers, + &[ComputeDriverKind::Podman], + "RPM default must pin compute_drivers to [podman] to prevent unexpected \ + driver selection when Docker is also installed" + ); + } } diff --git a/deploy/rpm/CONFIGURATION.md b/deploy/rpm/CONFIGURATION.md index b7fc5c6d6..8a7edca8c 100644 --- a/deploy/rpm/CONFIGURATION.md +++ b/deploy/rpm/CONFIGURATION.md @@ -6,11 +6,65 @@ the RPM package on Fedora and RHEL systems. For first-time setup, see QUICKSTART.md. For troubleshooting, see TROUBLESHOOTING.md. +## Default configuration + +The RPM ships a default TOML configuration template at +`/usr/share/openshell-gateway/gateway.toml.default`. On first start of +`openshell-gateway.service`, the systemd unit copies this template to +`~/.config/openshell/gateway.toml` if no config file exists there yet. + +The defaults are tuned for rootless Podman use: + +```toml +[openshell] +version = 1 + +[openshell.gateway] +bind_address = "0.0.0.0:17670" +compute_drivers = ["podman"] +``` + +`bind_address = "0.0.0.0:17670"` is required because Podman sandbox +containers reach the gateway over the host network bridge and cannot +connect to `127.0.0.1` inside the gateway's network namespace. mTLS is +enabled by default and protects all connections. + +`compute_drivers = ["podman"]` pins the compute driver to Podman. Without +this, the gateway auto-detects in order: Kubernetes, Podman, Docker. Pinning +prevents unexpected driver selection if Docker is also installed on the host. + +### Customizing the configuration + +Edit `~/.config/openshell/gateway.toml` directly. The template at +`/usr/share/openshell-gateway/gateway.toml.default` is not read at runtime +and is not overwritten by RPM upgrades. + +To apply environment variable overrides that persist across upgrades without +editing the TOML file, add them to `~/.config/openshell/gateway.env`: + +```shell +# Example: restrict to loopback only +OPENSHELL_BIND_ADDRESS=127.0.0.1 +``` + +To override the path to the TOML config file entirely: + +```shell +# In ~/.config/openshell/gateway.env +OPENSHELL_GATEWAY_CONFIG=/path/to/custom/gateway.toml +``` + +For one-off service overrides that persist across package upgrades: + +```shell +systemctl --user edit openshell-gateway +``` + ## TLS (mTLS) The RPM enables mutual TLS by default. The gateway requires a valid client certificate for all API connections and listens on -`127.0.0.1:17670` by default. +`0.0.0.0:17670` by default (see "Default configuration" above). ### Auto-generated certificates @@ -152,8 +206,8 @@ overrides that persist across package upgrades. | TOML option | Default | Description | |-------------|---------|-------------| -| `bind_address` | `127.0.0.1:17670` | Address for the gRPC/HTTP API. | -| `compute_drivers` | unset | When unset, the gateway auto-detects Kubernetes, then Podman, then Docker. Set `compute_drivers = ["podman"]` to force Podman. | +| `bind_address` | `0.0.0.0:17670` (RPM default) | Address for the gRPC/HTTP API. | +| `compute_drivers` | `["podman"]` (RPM default) | When unset, the gateway auto-detects Kubernetes, then Podman, then Docker. The RPM default pins to Podman. | | `default_image` | `ghcr.io/nvidia/openshell-community/sandboxes/base:latest` | Default sandbox image. | | `supervisor_image` | `ghcr.io/nvidia/openshell/supervisor:latest` | Supervisor image mounted into Podman sandboxes. | | `guest_tls_ca`, `guest_tls_cert`, `guest_tls_key` | auto-generated paths | Client TLS material bind-mounted into sandbox containers. | @@ -173,9 +227,8 @@ settings: version = 1 [openshell.gateway] -bind_address = "127.0.0.1:17670" -# Leave unset to auto-detect the compute driver. -# compute_drivers = ["podman"] +bind_address = "0.0.0.0:17670" +compute_drivers = ["podman"] default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest" [openshell.drivers.podman] @@ -239,7 +292,9 @@ For air-gapped environments: | Gateway binary | `/usr/bin/openshell-gateway` | | CLI binary | `/usr/bin/openshell` | | Systemd user unit | `/usr/lib/systemd/user/openshell-gateway.service` | +| Default TOML config template (read-only) | `/usr/share/openshell-gateway/gateway.toml.default` | +| Active gateway TOML configuration | `~/.config/openshell/gateway.toml` | +| Optional environment variable overrides | `~/.config/openshell/gateway.env` | | TLS certificates | `~/.local/state/openshell/tls/` | | CLI client certs | `~/.config/openshell/gateways/openshell/mtls/` | | Gateway database | `~/.local/state/openshell/gateway/openshell.db` | -| Optional gateway TOML configuration | `~/.config/openshell/gateway.toml` | diff --git a/deploy/rpm/QUICKSTART.md b/deploy/rpm/QUICKSTART.md index 72396329d..c6634ced9 100644 --- a/deploy/rpm/QUICKSTART.md +++ b/deploy/rpm/QUICKSTART.md @@ -65,10 +65,11 @@ On first start, the gateway automatically generates: - A self-signed PKI bundle (CA, server cert, client cert) for mTLS -> **Note:** The gateway binds to `127.0.0.1:17670` by default. Mutual -> TLS (mTLS) is enabled automatically on first start, requiring a valid -> client certificate for every connection. See CONFIGURATION.md for -> details. +> **Note:** The RPM default configuration binds to `0.0.0.0:17670` so +> Podman sandbox containers can reach the gateway over the host network +> bridge. Mutual TLS (mTLS) is enabled automatically on first start, +> requiring a valid client certificate for every connection. See +> CONFIGURATION.md for details. Verify the service is running: diff --git a/deploy/rpm/gateway.toml.default b/deploy/rpm/gateway.toml.default new file mode 100644 index 000000000..d85379964 --- /dev/null +++ b/deploy/rpm/gateway.toml.default @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Default gateway configuration for RPM installs. +# +# This file is seeded to ~/.config/openshell/gateway.toml on first start +# of the openshell-gateway.service systemd user unit. Edit that copy to +# customize. This file is not read directly at runtime. +# +# Configuration precedence (highest to lowest): +# CLI flag > OPENSHELL_* env var > TOML file > built-in default +# +# To override settings without editing this file, set OPENSHELL_* variables +# in ~/.config/openshell/gateway.env or run: +# systemctl --user edit openshell-gateway + +[openshell] +version = 1 + +[openshell.gateway] +# Podman sandbox containers reach the gateway over the host network bridge, +# which requires binding to all interfaces. Override to 127.0.0.1:17670 if +# you don't use Podman or want loopback-only access (e.g. behind a reverse +# proxy). mTLS is enabled by default and protects all connections. +bind_address = "0.0.0.0:17670" + +# Pin to the Podman compute driver. Without this, the gateway auto-detects +# in order: Kubernetes, Podman, Docker. Pinning prevents unexpected driver +# selection if Docker is also installed on the host. +compute_drivers = ["podman"] diff --git a/e2e/with-podman-gateway.sh b/e2e/with-podman-gateway.sh index d3b93e51a..875ebee4b 100755 --- a/e2e/with-podman-gateway.sh +++ b/e2e/with-podman-gateway.sh @@ -359,10 +359,18 @@ toml_string() { } GATEWAY_CONFIG="${STATE_DIR}/gateway.toml" + +# Start from the RPM default template so this e2e test exercises the same +# TOML config path that RPM users get on first start. The template sets +# bind_address = "0.0.0.0:17670" and compute_drivers = ["podman"]; those +# values must be correct for Podman e2e to pass, which means a regression +# to the template (wrong bind address, wrong driver) will surface here. +# +# We append the driver-specific table and override the port via CLI flag +# (CLI > TOML in the merge precedence) so the test can use an ephemeral port. +cp "${ROOT}/deploy/rpm/gateway.toml.default" "${GATEWAY_CONFIG}" { - printf '[openshell]\nversion = 1\n\n' - printf '[openshell.gateway]\nlog_level = "info"\n\n' - printf '[openshell.drivers.podman]\n' + printf '\n[openshell.drivers.podman]\n' # The Podman driver scopes isolation by network rather than namespace. printf 'network_name = %s\n' "$(toml_string "${PODMAN_NETWORK_NAME}")" printf 'gateway_port = %s\n' "${HOST_PORT}" @@ -380,14 +388,14 @@ GATEWAY_CONFIG="${STATE_DIR}/gateway.toml" if [ -n "${OPENSHELL_PODMAN_SOCKET:-}" ]; then printf 'socket_path = %s\n' "$(toml_string "${OPENSHELL_PODMAN_SOCKET}")" fi -} > "${GATEWAY_CONFIG}" +} >> "${GATEWAY_CONFIG}" GATEWAY_ARGS=( --config "${GATEWAY_CONFIG}" - --bind-address 0.0.0.0 + # bind_address and compute_drivers come from the RPM template; no CLI flags + # needed. Port is overridden via CLI (CLI > TOML) for ephemeral port selection. --port "${HOST_PORT}" --health-port "${HEALTH_PORT}" - --drivers podman --tls-cert "${PKI_DIR}/server/tls.crt" --tls-key "${PKI_DIR}/server/tls.key" --tls-client-ca "${PKI_DIR}/ca.crt" diff --git a/openshell.spec b/openshell.spec index c7c5d9204..e351f5a44 100644 --- a/openshell.spec +++ b/openshell.spec @@ -2,10 +2,11 @@ # SPDX-License-Identifier: Apache-2.0 %global crate openshell -%global openshell_cargo_version %{version} +%global openshell_version 0.0.37 +%global openshell_cargo_version %{openshell_version} # Python dist-info metadata intentionally follows the RPM Version. Dev build # identity is represented by Release for RPM packages. -%global openshell_python_version %{version} +%global openshell_python_version %{openshell_version} # Cargo/Rust builds with vendored deps do not produce debugsource listings # in the format redhat-rpm-config expects (especially on EPEL). @@ -18,14 +19,14 @@ %global image_tag dev Name: openshell -Version: 0.0.37 -Release: 1.20260506170246815148.rpm.dev.106.g99e94469%{?dist} +Version: %{openshell_version} +Release: 1.20260518180028805757.podman.toml.gateway.listener.11.g8c0cb7c8%{?dist} Summary: Safe, sandboxed runtimes for autonomous AI agents License: Apache-2.0 URL: https://github.com/NVIDIA/OpenShell -Source0: openshell-0.0.37.tar.gz -Source1: openshell-0.0.37-vendor.tar.xz +Source0: openshell-%{openshell_version}.tar.gz +Source1: openshell-%{openshell_version}-vendor.tar.xz ExclusiveArch: x86_64 aarch64 @@ -127,6 +128,11 @@ install -Dpm 0755 target/release/%{name} %{buildroot}%{_bindir}/%{name} # --- Gateway binary --- install -Dpm 0755 target/release/%{name}-gateway %{buildroot}%{_bindir}/%{name}-gateway +# --- Default gateway TOML config template --- +# Shipped as a read-only reference in %{_datadir}. The systemd unit seeds a +# user-level copy at ~/.config/openshell/gateway.toml on first start. +install -Dpm 0644 deploy/rpm/gateway.toml.default %{buildroot}%{_datadir}/%{name}-gateway/gateway.toml.default + # --- Gateway systemd user unit --- # Installed to the systemd user unit directory so any user can run: # systemctl --user enable --now openshell-gateway.service @@ -140,12 +146,17 @@ Wants=podman.socket [Service] Type=exec -# PKI is auto-generated on first start. Client certs are placed in -# ~/.config/openshell/gateways/openshell/mtls/ so the CLI discovers them -# automatically. Gateway runtime defaults are used unless a TOML config -# exists in the default user config location or OPENSHELL_GATEWAY_CONFIG is set. +# On first start the unit seeds a default TOML config and generates PKI. +# Client certs are placed in ~/.config/openshell/gateways/openshell/mtls/ so +# the CLI discovers them automatically. # See /usr/share/doc/openshell-gateway/ for details. +# Seed a default TOML config on first start if the user has not created one. +# The template ships at /usr/share/openshell-gateway/gateway.toml.default. +# Edit ~/.config/openshell/gateway.toml to customize. +# %%E expands to $XDG_CONFIG_HOME (~/.config) in user units. +ExecStartPre=/bin/sh -c 'test -f %%E/openshell/gateway.toml || install -Dm644 /usr/share/openshell-gateway/gateway.toml.default %%E/openshell/gateway.toml' + # Auto-generate PKI on first start if not present. # %%S expands to $XDG_STATE_HOME (~/.local/state) in user units. ExecStartPre=/usr/bin/openshell-gateway generate-certs --output-dir %%S/openshell/tls --server-san host.openshell.internal @@ -220,6 +231,15 @@ touch %{buildroot}%{python3_sitelib}/%{name}-%{openshell_python_version}.dist-in # build environment. PYTHONPATH=%{buildroot}%{python3_sitelib} %{python3} -c "from importlib.metadata import version; v = version('openshell'); print(v); assert v == '%{openshell_python_version}', f'expected %{openshell_python_version}, got {v}'" +# Verify the RPM default TOML config template was installed. +# A missing template means first-start seeding silently falls back to the +# binary default of 127.0.0.1, which breaks Podman sandbox connectivity. +test -f %{buildroot}%{_datadir}/%{name}-gateway/gateway.toml.default + +# Verify the systemd unit references the template in its ExecStartPre seed step. +# If this grep fails, the first-start seeding logic was removed from the unit. +grep -q 'gateway.toml.default' %{buildroot}%{_userunitdir}/%{name}-gateway.service + %post gateway %systemd_user_post %{name}-gateway.service @@ -246,6 +266,7 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} %{python3} -c "from importlib.metadata %doc %{_docdir}/%{name}-gateway/TROUBLESHOOTING.md %{_bindir}/%{name}-gateway %{_userunitdir}/%{name}-gateway.service +%{_datadir}/%{name}-gateway/gateway.toml.default %{_mandir}/man8/openshell-gateway.8* %files -n python3-%{name}