diff --git a/.github/workflows/build-cli.yml b/.github/workflows/build-cli.yml index 953244459c02..55e0a02b5281 100644 --- a/.github/workflows/build-cli.yml +++ b/.github/workflows/build-cli.yml @@ -6,6 +6,7 @@ # Platform Build Strategy: # - Linux standard: Uses native Ubuntu 22.04 runners to keep glibc compatibility with Ubuntu 22.04 LTS # - Linux Vulkan: Uses native Ubuntu 24.04 runners for newer Vulkan headers/tooling +# - Linux musl: Uses native Ubuntu 22.04 runners with reduced features for musl compatibility # - macOS: Uses native macOS runners for each architecture # - Windows: Uses Windows runner with native MSVC build on: @@ -42,6 +43,16 @@ jobs: target-suffix: unknown-linux-gnu build-on: ubuntu-22.04-arm variant: standard + - platform: linux + architecture: x86_64 + target-suffix: unknown-linux-musl + build-on: ubuntu-22.04 + variant: musl + - platform: linux + architecture: aarch64 + target-suffix: unknown-linux-musl + build-on: ubuntu-22.04-arm + variant: musl - platform: linux architecture: x86_64 target-suffix: unknown-linux-gnu @@ -104,6 +115,10 @@ jobs: glslc fi + if [ "${{ matrix.variant }}" = "musl" ]; then + sudo apt-get install -y musl-tools + fi + - name: Cache Cargo artifacts (Linux/macOS) if: matrix.platform != 'windows' uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 @@ -133,7 +148,14 @@ jobs: if [ "${{ matrix.variant }}" = "vulkan" ]; then FEATURE_ARGS=(--features vulkan) fi - cargo build --release --target ${TARGET} -p goose-cli "${FEATURE_ARGS[@]}" + + if [ "${{ matrix.variant }}" = "musl" ]; then + cargo build --release --target ${TARGET} -p goose-cli --bin goose \ + --no-default-features \ + --features portable-default + else + cargo build --release --target ${TARGET} -p goose-cli "${FEATURE_ARGS[@]}" + fi - name: Setup Rust (Windows) if: matrix.platform == 'windows' @@ -233,7 +255,7 @@ jobs: - name: Upload CLI artifact uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: - name: goose-${{ matrix.architecture }}-${{ matrix.target-suffix }}${{ matrix.variant != 'standard' && format('-{0}', matrix.variant) || '' }} + name: goose-${{ matrix.architecture }}-${{ matrix.target-suffix }}${{ matrix.variant != 'standard' && matrix.variant != 'musl' && format('-{0}', matrix.variant) || '' }} path: | ${{ env.ARTIFACT_BZ2 }} ${{ env.ARTIFACT_GZ }} diff --git a/.github/workflows/pr-comment-build-cli.yml b/.github/workflows/pr-comment-build-cli.yml index 431ba395fe50..f548f640cfb4 100644 --- a/.github/workflows/pr-comment-build-cli.yml +++ b/.github/workflows/pr-comment-build-cli.yml @@ -138,6 +138,8 @@ jobs: Download CLI builds for different platforms: - [📦 Linux (x86_64, Ubuntu 22.04-compatible)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-x86_64-unknown-linux-gnu.zip) - [📦 Linux (aarch64, Ubuntu 22.04-compatible)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-aarch64-unknown-linux-gnu.zip) + - [📦 Linux musl (x86_64)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-x86_64-unknown-linux-musl.zip) + - [📦 Linux musl (aarch64)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-aarch64-unknown-linux-musl.zip) - [📦 Linux Vulkan (x86_64, Ubuntu 24.04+)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-x86_64-unknown-linux-gnu-vulkan.zip) - [📦 Linux Vulkan (aarch64, Ubuntu 24.04+)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-aarch64-unknown-linux-gnu-vulkan.zip) - [📦 macOS (x86_64)](https://nightly.link/${{ github.repository }}/actions/runs/${{ github.run_id }}/goose-x86_64-apple-darwin.zip) diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index 38b0957b5f33..0bfe05e9afda 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -69,7 +69,15 @@ clap_complete_nushell = "4.6.0" winapi = { workspace = true } [features] -default = ["code-mode", "local-inference", "aws-providers", "telemetry", "otel", "rustls-tls"] +default = [ + "code-mode", + "local-inference", + "aws-providers", + "telemetry", + "otel", + "rustls-tls", + "system-keyring", +] code-mode = ["goose/code-mode"] local-inference = ["goose/local-inference"] aws-providers = ["goose/aws-providers"] @@ -77,6 +85,8 @@ cuda = ["goose/cuda", "local-inference"] vulkan = ["goose/vulkan", "local-inference"] telemetry = ["goose/telemetry"] otel = ["goose/otel"] +system-keyring = ["goose/system-keyring"] +portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel"] # disables the update command disable-update = [] rustls-tls = [ diff --git a/crates/goose-cli/src/commands/update.rs b/crates/goose-cli/src/commands/update.rs index 6cf2ba937121..732fc64648c1 100644 --- a/crates/goose-cli/src/commands/update.rs +++ b/crates/goose-cli/src/commands/update.rs @@ -18,14 +18,22 @@ fn asset_name() -> &'static str { { "goose-x86_64-apple-darwin.tar.bz2" } - #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "gnu"))] { "goose-x86_64-unknown-linux-gnu.tar.bz2" } - #[cfg(all(target_os = "linux", target_arch = "aarch64"))] + #[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "gnu"))] { "goose-aarch64-unknown-linux-gnu.tar.bz2" } + #[cfg(all(target_os = "linux", target_arch = "x86_64", target_env = "musl"))] + { + "goose-x86_64-unknown-linux-musl.tar.bz2" + } + #[cfg(all(target_os = "linux", target_arch = "aarch64", target_env = "musl"))] + { + "goose-aarch64-unknown-linux-musl.tar.bz2" + } #[cfg(all(target_os = "windows", target_arch = "x86_64", feature = "cuda"))] { "goose-x86_64-pc-windows-msvc-cuda.zip" diff --git a/crates/goose-server/Cargo.toml b/crates/goose-server/Cargo.toml index 47f10e249cf2..1077a071d5c8 100644 --- a/crates/goose-server/Cargo.toml +++ b/crates/goose-server/Cargo.toml @@ -12,7 +12,15 @@ description.workspace = true workspace = true [features] -default = ["code-mode", "local-inference", "aws-providers", "telemetry", "otel", "rustls-tls"] +default = [ + "code-mode", + "local-inference", + "aws-providers", + "telemetry", + "otel", + "rustls-tls", + "system-keyring", +] code-mode = ["goose/code-mode"] local-inference = ["goose/local-inference"] aws-providers = ["goose/aws-providers"] @@ -20,6 +28,8 @@ cuda = ["goose/cuda", "local-inference"] vulkan = ["goose/vulkan", "local-inference"] telemetry = ["goose/telemetry"] otel = ["goose/otel"] +system-keyring = ["goose/system-keyring"] +portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel"] rustls-tls = [ "reqwest/rustls", "tokio-tungstenite/rustls-tls-native-roots", diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 3b49beced2ce..c0c70a335c29 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -9,7 +9,15 @@ repository.workspace = true description.workspace = true [features] -default = ["code-mode", "local-inference", "aws-providers", "telemetry", "otel", "rustls-tls"] +default = [ + "code-mode", + "local-inference", + "aws-providers", + "telemetry", + "otel", + "rustls-tls", + "system-keyring", +] telemetry = [] otel = [ "dep:tracing-opentelemetry", @@ -59,6 +67,8 @@ native-tls = [ "oauth2/reqwest", "oauth2/native-tls", ] +system-keyring = ["dep:keyring"] +portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel"] [lints] @@ -110,7 +120,7 @@ opentelemetry_sdk = { workspace = true, optional = true } opentelemetry-appender-tracing = { workspace = true, optional = true } opentelemetry-otlp = { workspace = true, optional = true } opentelemetry-stdout = { workspace = true, optional = true } -keyring = { version = "3.6.2", features = ["vendored"] } +keyring = { version = "3.6.2", features = ["vendored"], optional = true } serde_yaml = { workspace = true } strum = { workspace = true } once_cell = { workspace = true } @@ -203,17 +213,17 @@ rustls = { version = "0.23", features = ["aws_lc_rs"], optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = { workspace = true } -keyring = { version = "3.6.2", features = ["windows-native"] } +keyring = { version = "3.6.2", features = ["windows-native"], optional = true } # Platform-specific GPU acceleration for Whisper and local inference [target.'cfg(target_os = "macos")'.dependencies] candle-core = { version = "0.10.2", default-features = false, features = ["metal"], optional = true } candle-nn = { version = "0.10.2", default-features = false, features = ["metal"], optional = true } llama-cpp-2 = { version = "0.1.145", features = ["sampler", "metal", "mtmd"], optional = true } -keyring = { version = "3.6.2", features = ["apple-native"] } +keyring = { version = "3.6.2", features = ["apple-native"], optional = true } [target.'cfg(target_os = "linux")'.dependencies] -keyring = { version = "3.6.2", features = ["sync-secret-service"] } +keyring = { version = "3.6.2", features = ["sync-secret-service"], optional = true } libc = "0.2.186" [dev-dependencies] diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs index acead899de11..6121adfbbf0e 100644 --- a/crates/goose/src/config/base.rs +++ b/crates/goose/src/config/base.rs @@ -1,6 +1,7 @@ use crate::config::paths::Paths; use crate::config::GooseMode; use fs2::FileExt; +#[cfg(feature = "system-keyring")] use keyring::Entry; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; @@ -34,7 +35,9 @@ fn write_secrets_file(path: &Path, content: &str) -> std::io::Result<()> { } } +#[cfg(feature = "system-keyring")] const KEYRING_SERVICE: &str = "goose"; +#[cfg(feature = "system-keyring")] const KEYRING_USERNAME: &str = "secrets"; pub const CONFIG_YAML_NAME: &str = "config.yaml"; @@ -68,6 +71,7 @@ impl From for ConfigError { } } +#[cfg(feature = "system-keyring")] impl From for ConfigError { fn from(err: keyring::Error) -> Self { ConfigError::KeyringError(err.to_string()) @@ -131,8 +135,13 @@ pub struct Config { } enum SecretStorage { - Keyring { service: String }, - File { path: PathBuf }, + #[cfg(feature = "system-keyring")] + Keyring { + service: String, + }, + File { + path: PathBuf, + }, } // Global instance @@ -175,19 +184,11 @@ impl Default for Config { secrets_cache: Arc::new(Mutex::new(None)), }; - let secrets = if env::var("GOOSE_DISABLE_KEYRING").is_ok() + let keyring_disabled = env::var("GOOSE_DISABLE_KEYRING").is_ok() || no_secrets_config .get_param::("GOOSE_DISABLE_KEYRING") - .is_ok_and(|v| keyring_disabled_value(&v)) - { - SecretStorage::File { - path: config_dir.join("secrets.yaml"), - } - } else { - SecretStorage::Keyring { - service: KEYRING_SERVICE.to_string(), - } - }; + .is_ok_and(|v| keyring_disabled_value(&v)); + let secrets = secret_storage(&config_dir, keyring_disabled, default_keyring_service()); Self { config_paths, secrets, @@ -337,6 +338,40 @@ fn keyring_disabled_in_config(config_path: &Path) -> bool { .unwrap_or(false) } +#[cfg(feature = "system-keyring")] +fn default_keyring_service() -> &'static str { + KEYRING_SERVICE +} + +#[cfg(not(feature = "system-keyring"))] +fn default_keyring_service() -> &'static str { + "" +} + +fn secrets_file_path_in(config_dir: &Path) -> PathBuf { + config_dir.join("secrets.yaml") +} + +#[cfg(feature = "system-keyring")] +fn secret_storage(config_dir: &Path, keyring_disabled: bool, service: &str) -> SecretStorage { + if keyring_disabled { + SecretStorage::File { + path: secrets_file_path_in(config_dir), + } + } else { + SecretStorage::Keyring { + service: service.to_string(), + } + } +} + +#[cfg(not(feature = "system-keyring"))] +fn secret_storage(config_dir: &Path, _keyring_disabled: bool, _service: &str) -> SecretStorage { + SecretStorage::File { + path: secrets_file_path_in(config_dir), + } +} + impl Config { /// Get the global configuration instance. /// @@ -352,21 +387,13 @@ impl Config { /// to manage multiple configuration files. pub fn new>(config_path: P, service: &str) -> Result { let config_path = config_path.as_ref().to_path_buf(); - let secrets = if env::var("GOOSE_DISABLE_KEYRING").is_ok() - || keyring_disabled_in_config(&config_path) - { - let config_dir = config_path - .parent() - .map(Path::to_path_buf) - .unwrap_or_else(Paths::config_dir); - SecretStorage::File { - path: config_dir.join("secrets.yaml"), - } - } else { - SecretStorage::Keyring { - service: service.to_string(), - } - }; + let keyring_disabled = + env::var("GOOSE_DISABLE_KEYRING").is_ok() || keyring_disabled_in_config(&config_path); + let config_dir = config_path + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(Paths::config_dir); + let secrets = secret_storage(&config_dir, keyring_disabled, service); Ok(Config { config_paths: vec![config_path], secrets, @@ -578,6 +605,7 @@ impl Config { tracing::debug!("secrets cache miss, fetching from storage"); let loaded = match &self.secrets { + #[cfg(feature = "system-keyring")] SecretStorage::Keyring { service } => { let result = self.handle_keyring_operation(|entry| entry.get_password(), service, None); @@ -819,6 +847,7 @@ impl Config { fn write_all_secrets(&self, values: &HashMap) -> Result<(), ConfigError> { match &self.secrets { + #[cfg(feature = "system-keyring")] SecretStorage::Keyring { service } => { let json_value = serde_json::to_string(values)?; match self.handle_keyring_operation( @@ -937,17 +966,20 @@ impl Config { } /// Get the path to the secrets storage file + #[cfg(feature = "system-keyring")] fn secrets_file_path() -> PathBuf { - Paths::config_dir().join("secrets.yaml") + secrets_file_path_in(&Paths::config_dir()) } /// Fall back to file storage when keyring is unavailable + #[cfg(feature = "system-keyring")] fn fallback_to_file_storage(&self) -> Result, ConfigError> { let path = Self::secrets_file_path(); self.read_secrets_from_file(&path) } /// Write secrets to file storage (used for fallback) + #[cfg(feature = "system-keyring")] fn write_secrets_to_file(&self, values: &HashMap) -> Result<(), ConfigError> { std::fs::create_dir_all(Paths::config_dir())?; let path = Self::secrets_file_path(); @@ -962,6 +994,7 @@ impl Config { } /// Check if an error string indicates a keyring availability issue that should trigger fallback + #[cfg(feature = "system-keyring")] fn is_keyring_availability_error(&self, error_str: &str) -> bool { let lower = error_str.to_lowercase(); lower.contains("keyring") @@ -972,11 +1005,13 @@ impl Config { } /// Get a keyring entry for the specified service + #[cfg(feature = "system-keyring")] fn get_keyring_entry(service: &str) -> Result { Entry::new(service, KEYRING_USERNAME) } /// Handle keyring errors with automatic fallback to file storage + #[cfg(feature = "system-keyring")] fn handle_keyring_fallback_error( &self, keyring_err: &keyring::Error, @@ -998,6 +1033,7 @@ impl Config { } /// Handle keyring operation with automatic fallback to file storage + #[cfg(feature = "system-keyring")] fn handle_keyring_operation( &self, operation: impl FnOnce(keyring::Entry) -> Result, diff --git a/crates/goose/src/providers/toolshim.rs b/crates/goose/src/providers/toolshim.rs index 92c174d62827..6ea6cebe7e48 100644 --- a/crates/goose/src/providers/toolshim.rs +++ b/crates/goose/src/providers/toolshim.rs @@ -31,6 +31,7 @@ //! use super::errors::ProviderError; +#[cfg(feature = "local-inference")] use super::local_inference::LOCAL_LLM_MODEL_CONFIG_KEY; use super::ollama::OLLAMA_DEFAULT_PORT; use super::ollama::OLLAMA_HOST; @@ -52,6 +53,8 @@ use uuid::Uuid; pub const DEFAULT_INTERPRETER_MODEL_OLLAMA: &str = "mistral-nemo"; pub const TOOLSHIM_BACKEND_ENV_VAR: &str = "GOOSE_TOOLSHIM_BACKEND"; pub const TOOLSHIM_LOCAL_MODEL_ENV_VAR: &str = "GOOSE_TOOLSHIM_MODEL"; +#[cfg(not(feature = "local-inference"))] +const LOCAL_LLM_MODEL_CONFIG_KEY: &str = "LOCAL_LLM_MODEL"; const TOOL_CALLS_SECTION_BEGIN: &str = "<|tool_calls_section_begin|>"; const TOOL_CALLS_SECTION_END: &str = "<|tool_calls_section_end|>"; diff --git a/download_cli.sh b/download_cli.sh index 7131611c7425..a995d1d09602 100755 --- a/download_cli.sh +++ b/download_cli.sh @@ -18,7 +18,7 @@ set -eu # GOOSE_VERSION - Optional: specific version to install (e.g., "v1.0.25"). Overrides CANARY. Can be in the format vX.Y.Z, vX.Y.Z-suffix, or X.Y.Z # GOOSE_PROVIDER - Optional: provider for goose # GOOSE_MODEL - Optional: model for goose -# GOOSE_LINUX_VARIANT - Optional: Linux package variant to install (`standard` or `vulkan`) +# GOOSE_LINUX_VARIANT - Optional: Linux package variant to install (`standard`, `vulkan`, or `musl`) # GOOSE_WINDOWS_VARIANT - Optional: Windows package variant to install (`standard` or `cuda`) # CANARY - Optional: if set to "true", downloads from canary release instead of stable # CONFIGURE - Optional: if set to "false", disables running goose configure interactively @@ -69,7 +69,7 @@ fi GOOSE_BIN_DIR="${GOOSE_BIN_DIR:-$DEFAULT_BIN_DIR}" RELEASE="${CANARY:-false}" CONFIGURE="${CONFIGURE:-true}" -GOOSE_LINUX_VARIANT="${GOOSE_LINUX_VARIANT:-standard}" +GOOSE_LINUX_VARIANT="${GOOSE_LINUX_VARIANT:-}" GOOSE_WINDOWS_VARIANT="${GOOSE_WINDOWS_VARIANT:-standard}" if [ -n "${GOOSE_VERSION:-}" ]; then # Validate the version format @@ -141,6 +141,28 @@ case "$ARCH" in ;; esac +detect_linux_musl() { + if [[ "$OSTYPE" == "linux-musl"* ]]; then + return 0 + fi + + if command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then + return 0 + fi + + return 1 +} + +if [ "$OS" = "linux" ] && [ -z "$GOOSE_LINUX_VARIANT" ]; then + if detect_linux_musl; then + GOOSE_LINUX_VARIANT="musl" + else + GOOSE_LINUX_VARIANT="standard" + fi +elif [ -z "$GOOSE_LINUX_VARIANT" ]; then + GOOSE_LINUX_VARIANT="standard" +fi + # Debug output (safely handle undefined variables) echo "WINDIR: ${WINDIR:-}" echo "OSTYPE: $OSTYPE" @@ -176,15 +198,17 @@ elif [ "$OS" = "windows" ]; then OUT_FILE="goose.exe" else case "$GOOSE_LINUX_VARIANT" in - standard|vulkan) ;; + standard|vulkan|musl) ;; *) - echo "Error: Unsupported GOOSE_LINUX_VARIANT '$GOOSE_LINUX_VARIANT'. Expected 'standard' or 'vulkan'." + echo "Error: Unsupported GOOSE_LINUX_VARIANT '$GOOSE_LINUX_VARIANT'. Expected 'standard', 'vulkan', or 'musl'." exit 1 ;; esac FILE="goose-$ARCH-unknown-linux-gnu.tar.bz2" if [ "$GOOSE_LINUX_VARIANT" = "vulkan" ]; then FILE="goose-$ARCH-unknown-linux-gnu-vulkan.tar.bz2" + elif [ "$GOOSE_LINUX_VARIANT" = "musl" ]; then + FILE="goose-$ARCH-unknown-linux-musl.tar.bz2" fi EXTRACT_CMD="tar" fi