Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .github/workflows/rpm-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,44 @@ jobs:
echo "=== Built RPMs ==="
ls -lah artifacts/
- name: Verify runtime-neutral RPM installation
run: |
set -euo pipefail
cli_rpm=""
gateway_rpm=""
for rpm_file in artifacts/*.rpm; do
case "$(rpm -qp --queryformat '%{NAME}' "$rpm_file")" in
openshell) cli_rpm="$rpm_file" ;;
openshell-gateway) gateway_rpm="$rpm_file" ;;
esac
done
test -n "$cli_rpm"
test -n "$gateway_rpm"
if rpm -qp --recommends "$cli_rpm" | grep -Eq '^podman([[:space:]]|$)'; then
echo "::error::The openshell RPM still recommends Podman"
exit 1
fi
if rpm -qp --requires "$gateway_rpm" | grep -Eq '^podman([[:space:]]|$)'; then
echo "::error::The openshell-gateway RPM still requires Podman"
exit 1
fi
if rpm -q podman >/dev/null 2>&1; then
dnf remove -y podman
fi
if rpm -q podman >/dev/null 2>&1; then
echo "::error::Podman must be absent before the package install test"
exit 1
fi
dnf install -y "$cli_rpm" "$gateway_rpm"
if rpm -q podman >/dev/null 2>&1; then
echo "::error::Installing OpenShell unexpectedly installed Podman"
exit 1
fi
- name: Upload RPM artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yml = "0.0.12"
toml = "0.8"
toml_edit = "0.22"
apollo-parser = "0.8.5"
tower-mcp-types = "0.12.0"

Expand Down
7 changes: 4 additions & 3 deletions architecture/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,10 @@ Runtime layout:
gateway binaries must not reference `GLIBC_*` symbols newer than
`GLIBC_2.28`; release workflows verify this before publishing artifacts. The
gateway bundles z3, so the image does not need a distro-provided z3 runtime.
- **VM driver**: host GNU-linked binary installed at
`/usr/libexec/openshell/openshell-driver-vm` in Linux packages and published
as a release artifact. Linux GNU VM driver binaries must not reference
- **VM driver**: host GNU-linked binary included at
`/usr/libexec/openshell/openshell-driver-vm` in Debian packages and published
as a standalone release artifact for RPM hosts. Homebrew installs it in the
formula libexec directory; RPM and Snap do not bundle it. Linux GNU VM driver binaries must not reference
`GLIBC_*` symbols newer than `GLIBC_2.28`; release workflows verify this
before publishing artifacts.
- **Supervisor**: `scratch` base, static musl binary at `/openshell-sandbox`.
Expand Down
33 changes: 33 additions & 0 deletions architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,27 @@ requested present -> generate and write. This guards continuity across restarts
and upgrades while still recovering cleanly if an operator deletes everything
and starts over.

### Package installer invariants

Package installers follow these boundaries across Debian, RPM, and Homebrew:

- Select the supported native package method deterministically for the host.
- Install matching OpenShell CLI and gateway artifacts in the package manager's
standard executable path.
- Install and enable the gateway as a supervised user service.
- Preserve existing gateway configuration and state across installs and
upgrades.
- Keep the listener on loopback with TLS unless the operator changes it.
- Do not install, remove, start, or configure optional Docker and Podman
runtimes.
- Leave compute-driver selection automatic unless the operator explicitly pins
a driver.
- Treat package installation and gateway health as separate outcomes. A
gateway startup failure leaves the package and service installed, returns a
nonzero installer status, and prints recovery instructions.
- Remain safe to rerun after the operator fixes an optional runtime or service
prerequisite.

Operators who manage TLS PKI with cert-manager enable `certManager.enabled`;
cert-manager takes precedence over built-in TLS generation and the chart still
renders the JWT-only hook. Operators who pre-create all TLS and JWT Secrets can
Expand All @@ -448,6 +469,18 @@ Driver implementation settings live in the TOML driver tables. See
`docs/reference/gateway-config.mdx` for worked per-driver examples and RFC
0003 for the full schema.

`openshell-gateway config detect-driver` exposes the gateway's automatic driver
detection as a side-effect-free, machine-readable command.
`openshell-gateway config set` provides typed, comment-preserving updates for
operator-managed settings. It resolves the same explicit or XDG config path as
gateway startup, validates the complete document, and replaces it atomically.
Service environment overrides remain an operator escape hatch because they
take precedence over later TOML edits.

When selection remains automatic, the gateway probes available runtimes at
every process start. Runtime-specific recovery guidance belongs to gateway and
installer diagnostics so it stays synchronized with detection behavior.

`database_url` is env-only and rejected when present in the file
(`OPENSHELL_DB_URL` / `--db-url`).

Expand Down
26 changes: 18 additions & 8 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,30 +113,40 @@ impl FromStr for ComputeDriverKind {
}
}

/// Auto-detect the appropriate compute driver based on the runtime environment.
/// Detect available compute drivers based on the runtime environment.
///
/// Priority order: Kubernetes → Podman → Docker.
/// VM is never auto-detected (requires explicit `--drivers vm`).
///
/// Returns the first driver where the environment check passes.
/// Returns `None` if no compatible driver is found.
pub fn detect_driver() -> Option<ComputeDriverKind> {
/// Returns every available driver in selection priority order.
///
/// VM is excluded because it requires explicit operator selection.
#[must_use]
pub fn detect_drivers() -> Vec<ComputeDriverKind> {
let mut drivers = Vec::new();

// Kubernetes: check for KUBERNETES_SERVICE_HOST env var (set inside pods)
if std::env::var_os("KUBERNETES_SERVICE_HOST").is_some() {
return Some(ComputeDriverKind::Kubernetes);
drivers.push(ComputeDriverKind::Kubernetes);
}

// Podman: check for a reachable local API socket.
if is_podman_available() {
return Some(ComputeDriverKind::Podman);
drivers.push(ComputeDriverKind::Podman);
}

// Docker: check if the CLI is available or a local Docker socket exists.
if is_docker_available() {
return Some(ComputeDriverKind::Docker);
drivers.push(ComputeDriverKind::Docker);
}

None
drivers
}

/// Returns the first available driver in automatic selection priority order.
#[must_use]
pub fn detect_driver() -> Option<ComputeDriverKind> {
detect_drivers().into_iter().next()
}

/// Check if a binary is available on the system PATH.
Expand Down
8 changes: 7 additions & 1 deletion crates/openshell-driver-vm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,13 @@ auto-detection. Set `OPENSHELL_DRIVERS=vm` to force the VM driver.

On RPM-family Linux x86_64 and aarch64 systems, `install.sh` installs the
`openshell` and `openshell-gateway` RPM packages from the selected release tag.
The RPM gateway package is configured for the Podman driver.
The RPM does not include `openshell-driver-vm`. Install the matching standalone
release artifact under one of the directories the gateway searches
(`~/.local/libexec/openshell`, `/usr/libexec/openshell`,
`/usr/local/libexec/openshell`, or `/usr/local/libexec`), or set
`[openshell.drivers.vm].driver_dir` to its location. The RPM leaves compute
driver selection unset, so the gateway auto-detects Podman or Docker unless VM
is selected explicitly.

On Apple Silicon macOS, `install.sh` stages the generated `openshell.rb`
formula from the selected release in the `nvidia/openshell` Homebrew tap.
Expand Down
3 changes: 2 additions & 1 deletion crates/openshell-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ pin-project-lite = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
toml = { workspace = true }
toml_edit = { workspace = true }
tempfile = "3"
tokio-stream = { workspace = true }
sqlx = { workspace = true }
reqwest = { workspace = true }
Expand All @@ -92,7 +94,6 @@ russh = "0.57"
rand = { workspace = true }
petname = "2"
ipnet = "2"
tempfile = "3"
rustix = { workspace = true }
x509-parser = "0.16"
arc-swap = "1"
Expand Down
72 changes: 67 additions & 5 deletions crates/openshell-server/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,19 @@ struct Cli {
enum Commands {
/// Generate mTLS PKI and write Kubernetes Secrets (Helm pre-install hook).
GenerateCerts(certgen::CertgenArgs),
/// Inspect or update the gateway TOML configuration.
Config(crate::config_edit::ConfigArgs),
}

#[derive(clap::Args, Debug)]
#[allow(clippy::struct_excessive_bools)]
struct RunArgs {
/// Path to a TOML configuration file (see RFC 0003).
/// Path to the gateway TOML configuration file (see RFC 0003).
///
/// When set, gateway-wide settings and per-driver tables are read from
/// the file. Gateway command-line flags and `OPENSHELL_*` environment
/// variables continue to take precedence over gateway file values.
#[arg(long, env = "OPENSHELL_GATEWAY_CONFIG")]
/// Gateway startup reads this file. Config subcommands update it. Gateway
/// command-line flags and `OPENSHELL_*` environment variables continue to
/// take precedence over file values at runtime.
#[arg(long, env = "OPENSHELL_GATEWAY_CONFIG", global = true)]
config: Option<PathBuf>,

/// IP address to bind the server, health, and metrics listeners to.
Expand Down Expand Up @@ -228,6 +230,7 @@ pub async fn run_cli() -> Result<()> {

match cli.command {
Some(Commands::GenerateCerts(args)) => certgen::run(args).await,
Some(Commands::Config(args)) => crate::config_edit::run(args, cli.run.config),
None => Box::pin(run_from_args(cli.run, matches)).await,
}
}
Expand Down Expand Up @@ -1075,6 +1078,65 @@ mod tests {
));
}

#[test]
fn config_set_uses_explicit_config_path() {
let _lock = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _g = EnvVarGuard::remove("OPENSHELL_GATEWAY_CONFIG");
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("custom/gateway.toml");
let path_string = path.to_string_lossy().into_owned();

let cli = Cli::try_parse_from([
"openshell-gateway",
"config",
"set",
"--config",
&path_string,
"--compute-driver",
"podman",
"--bind-address",
"0.0.0.0:17670",
])
.expect("config set should parse without runtime arguments");
let Cli { command, run } = cli;
let Some(super::Commands::Config(args)) = command else {
panic!("expected config subcommand");
};

crate::config_edit::run(args, run.config).unwrap();

let loaded = crate::config_file::load(&path).unwrap();
assert_eq!(
loaded.openshell.gateway.compute_drivers,
Some(vec!["podman".to_string()])
);
assert_eq!(
loaded.openshell.gateway.bind_address,
Some("0.0.0.0:17670".parse().unwrap())
);
}

#[test]
fn config_set_requires_at_least_one_setting() {
let error = Cli::try_parse_from(["openshell-gateway", "config", "set"])
.expect_err("config set without a setting should fail");

assert_eq!(
error.kind(),
clap::error::ErrorKind::MissingRequiredArgument
);
}

#[test]
fn config_detect_driver_parses_without_runtime_arguments() {
let cli = Cli::try_parse_from(["openshell-gateway", "config", "detect-driver"])
.expect("config detect-driver should parse");

assert!(matches!(cli.command, Some(super::Commands::Config(_))));
}

#[test]
fn bare_invocation_with_no_db_url_parses_for_runtime_defaults() {
// db_url is Option<String> at the clap level so subcommand parsing
Expand Down
Loading
Loading