diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9da3d6a2..c9688fc1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -336,11 +336,11 @@ jobs: test -f "$HOME/.orbit/embed/models/bge-small/orbit-model.json" smoke-install-ubuntu: - name: Smoke test installer on Ubuntu 22.04 + name: Smoke test installer on Ubuntu 24.04 needs: publish-release - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 container: - image: ubuntu:22.04 + image: ubuntu:24.04 steps: - name: Install prerequisites shell: bash diff --git a/README.md b/README.md index 3d64d908..800220bc 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Paste the prompt below into your agent (Claude Code, Codex CLI, or Gemini CLI) * > 3. Clone `https://github.com/danieljhkim/orbit` into the location from step 1, then run `make install`. This builds with cargo and copies the `orbit` binary to `$INSTALL_BIN_DIR` (default: `~/.cargo/bin`). Confirm the install path with me before running. Verify with `orbit --version`. > 4. Run `orbit init` to initialize global state at `~/.orbit`. > 5. From *this* repository (not the Orbit clone), run `orbit workspace init --mcp`. This creates `.orbit/` here and auto-registers Orbit's MCP server with installed agent CLIs (Claude Code, Codex, Gemini). -> 6. Ask me whether to enable semantic search (**optional**). `orbit semantic install` downloads a small embedder companion plus the default bge-small model (lives under `~/.orbit/embed/`) and powers `orbit search --hybrid` / `orbit search similar ` over tasks. Don't install without my OK. If I accept and tasks already exist in this workspace, also run `orbit semantic index` to backfill the corpus. +> 6. Ask me whether to enable semantic search (**optional**). `orbit semantic install` downloads a small embedder companion plus the default bge-small model (lives under `~/.orbit/embed/`) and powers `orbit search --hybrid` / `orbit search similar ` over tasks. It requires macOS arm64 or Linux x86_64/aarch64 with glibc >= 2.38; Intel macOS is unsupported for semantic search. Don't install without my OK. If I accept and tasks already exist in this workspace, also run `orbit semantic index` to backfill the corpus. > 7. Read the key documents so you actually understand the model: > - `README.md` — feature surface, install model, plugin vs CLI > - `docs/POSITIONING.md` — what Orbit is for, what it isn't (especially "who this is for") @@ -137,6 +137,11 @@ Customizing crews (which model runs planner/implementer/reviewer), the base bran `orbit search` is the unified query surface for tasks, docs, learnings, and ADRs. It defaults to lexical matching. Opt into hybrid embedding ranking over task fields or indexed docs with `--hybrid`, or find cosine-neighbor tasks with `orbit search similar `. The embedder runs as a separate companion subprocess, so semantic search has zero cost when unused. +The semantic companion is released for macOS arm64 and Linux x86_64/aarch64 +with glibc >= 2.38 (Ubuntu 24.04 or equivalent). Intel macOS can run the +Orbit CLI, but semantic search is unsupported because there is no Intel macOS +companion asset. + ```bash orbit semantic install # one-time: download companion + default model (bge-small) orbit semantic index # backfill existing tasks diff --git a/crates/orbit-core/assets/skills/orbit-search/SKILL.md b/crates/orbit-core/assets/skills/orbit-search/SKILL.md index 80df395c..9ece7ca7 100644 --- a/crates/orbit-core/assets/skills/orbit-search/SKILL.md +++ b/crates/orbit-core/assets/skills/orbit-search/SKILL.md @@ -76,6 +76,10 @@ Do not use search for "find every symbol matching pattern X"; use `orbit.graph.s | Show status | CLI-only | `orbit semantic stats` | | Rebuild embeddings | CLI-only | `orbit semantic index --kind tasks|docs|learnings|adrs|all [--model MODEL] [--force]` | +The released semantic companion supports macOS arm64 and Linux x86_64/aarch64 +with glibc >= 2.38. Intel macOS can run the CLI, but semantic search is +unsupported because there is no x86_64-apple-darwin companion asset. + Do not run `install` without operator consent. If a semantic query fails because the companion is missing, fall back to plain `orbit search --kind ` (lexical) and continue unless the user explicitly asked to enable embeddings. ## Result Shape diff --git a/crates/orbit-search/src/commands/install.rs b/crates/orbit-search/src/commands/install.rs index b8cf8f5b..6ca8d169 100644 --- a/crates/orbit-search/src/commands/install.rs +++ b/crates/orbit-search/src/commands/install.rs @@ -28,8 +28,9 @@ use sha2::{Digest, Sha256}; use crate::commands::{DEFAULT_RELEASE_BASE_URL, parse_model}; use crate::companion::{ - COMPANION_OVERRIDE_ENV, UNSAFE_COMPANION_OVERRIDE_ENV, unsafe_companion_overrides_enabled, - validate_companion_override_path, validate_managed_companion_path, + COMPANION_OVERRIDE_ENV, UNSAFE_COMPANION_OVERRIDE_ENV, ensure_semantic_search_supported, + unsafe_companion_overrides_enabled, validate_companion_override_path, + validate_managed_companion_path, }; use crate::{CompanionPaths, platform_companion_filename}; @@ -203,7 +204,13 @@ pub(crate) fn resolve_download_source() -> Result Result { let url = format!("{DEFAULT_RELEASE_BASE_URL}/{asset_name}"); validate_download_url(&url)?; Ok(CompanionDownloadSource { diff --git a/crates/orbit-search/src/commands/tests/install.rs b/crates/orbit-search/src/commands/tests/install.rs index 660fb34b..4038890e 100644 --- a/crates/orbit-search/src/commands/tests/install.rs +++ b/crates/orbit-search/src/commands/tests/install.rs @@ -9,11 +9,13 @@ use super::super::install::path_execution_fallback_rationale; use super::super::install::{ CompanionIntegrity, CompanionLaunchMode, ManagedCompanion, SemanticInstallParams, - checksum_from_manifest, companion_launch_mode, resolve_download_source, run, sha256_hex, - verify_release_checksum_signature_with_key, + checksum_from_manifest, companion_launch_mode, default_release_download_source, run, + sha256_hex, verify_release_checksum_signature_with_key, }; -use crate::companion::unsafe_companion_overrides_enabled; +use crate::companion::{ + ensure_semantic_search_supported_for_platform, unsafe_companion_overrides_enabled, +}; use crate::{CompanionPaths, locate_companion, platform_companion_filename}; use std::path::PathBuf; use std::sync::{Mutex, MutexGuard, OnceLock}; @@ -190,6 +192,41 @@ fn companion_launch_mode_matches_platform_support() { } } +#[test] +fn semantic_install_rejects_intel_mac_platform_before_download() { + let error = ensure_semantic_search_supported_for_platform("macos-x86_64", None) + .expect_err("Intel macOS should not have a released semantic companion"); + + assert!( + error + .to_string() + .contains("semantic search unsupported on this platform (macos-x86_64)"), + "{error}" + ); +} + +#[test] +fn semantic_install_accepts_released_companion_platforms() { + ensure_semantic_search_supported_for_platform("macos-aarch64", None) + .expect("macOS arm64 companion is released"); + ensure_semantic_search_supported_for_platform("linux-x86_64", Some("2.38")) + .expect("Linux x86_64 companion is released on glibc 2.38+"); + ensure_semantic_search_supported_for_platform("linux-aarch64", Some("2.39")) + .expect("Linux aarch64 companion is released on glibc 2.38+"); +} + +#[test] +fn semantic_install_rejects_linux_glibc_below_floor() { + let error = ensure_semantic_search_supported_for_platform("linux-x86_64", Some("2.35")) + .expect_err("glibc 2.35 should be below the companion runtime floor"); + + assert!( + error.to_string().contains("requires glibc >= 2.38"), + "{error}" + ); + assert!(error.to_string().contains("detected glibc 2.35"), "{error}"); +} + #[test] #[cfg(unix)] fn checksum_mismatch_rejects_replacement_before_install() { @@ -253,7 +290,8 @@ fn default_download_source_requires_release_checksum_manifest() { remove_env("ORBIT_SEARCH_COMPANION_SHA256"); remove_env("ORBIT_SEARCH_COMPANION_ALLOW_UNSAFE"); - let source = resolve_download_source().expect("default source"); + let source = default_release_download_source("orbit-search-companion-linux-x86_64".to_string()) + .expect("default source"); match source.integrity { CompanionIntegrity::ReleaseSignedChecksum { @@ -263,7 +301,7 @@ fn default_download_source_requires_release_checksum_manifest() { } => { assert!(checksums_url.ends_with("/orbit-checksums.txt")); assert!(signature_url.ends_with("/orbit-checksums.txt.sig")); - assert_eq!(asset_name, platform_companion_filename()); + assert_eq!(asset_name, "orbit-search-companion-linux-x86_64"); } other => panic!("default source should require signed release checksum: {other:?}"), } diff --git a/crates/orbit-search/src/companion.rs b/crates/orbit-search/src/companion.rs index b1301dd2..cf210454 100644 --- a/crates/orbit-search/src/companion.rs +++ b/crates/orbit-search/src/companion.rs @@ -7,6 +7,8 @@ //! `CompanionNotInstalled` shape so callers can surface a clean install hint. use std::env; +#[cfg(all(target_os = "linux", target_env = "gnu"))] +use std::ffi::CStr; use std::fs; use std::path::{Path, PathBuf}; @@ -15,6 +17,9 @@ use orbit_common::types::OrbitError; pub(crate) const COMPANION_OVERRIDE_ENV: &str = "ORBIT_SEARCH_COMPANION"; pub(crate) const UNSAFE_COMPANION_OVERRIDE_ENV: &str = "ORBIT_SEARCH_COMPANION_ALLOW_UNSAFE"; pub const INSTALL_REMEDIATION: &str = "Semantic search not enabled. Run `orbit semantic install` to download the inference companion."; +const MIN_LINUX_GLIBC_MAJOR: u32 = 2; +const MIN_LINUX_GLIBC_MINOR: u32 = 38; +const MIN_LINUX_GLIBC_DISPLAY: &str = "2.38"; #[derive(Debug, Clone)] pub struct CompanionPaths { @@ -70,6 +75,75 @@ pub fn platform_id() -> &'static str { } } +pub(crate) fn ensure_semantic_search_supported() -> Result<(), OrbitError> { + let glibc_version = current_glibc_version(); + ensure_semantic_search_supported_for_platform(platform_id(), glibc_version.as_deref()) +} + +pub(crate) fn ensure_semantic_search_supported_for_platform( + platform: &str, + glibc_version: Option<&str>, +) -> Result<(), OrbitError> { + match platform { + "macos-aarch64" => Ok(()), + "linux-aarch64" | "linux-x86_64" => ensure_linux_glibc_supported(platform, glibc_version), + _ => Err(unsupported_semantic_platform(platform)), + } +} + +fn ensure_linux_glibc_supported( + platform: &str, + glibc_version: Option<&str>, +) -> Result<(), OrbitError> { + let Some(glibc_version) = glibc_version else { + return Ok(()); + }; + let Some((major, minor)) = parse_glibc_major_minor(glibc_version) else { + return Ok(()); + }; + if (major, minor) < (MIN_LINUX_GLIBC_MAJOR, MIN_LINUX_GLIBC_MINOR) { + return Err(OrbitError::InvalidInput(format!( + "semantic search unsupported on this Linux system ({platform}): orbit-search-companion requires glibc >= {MIN_LINUX_GLIBC_DISPLAY}; detected glibc {glibc_version}. Use Ubuntu 24.04 or another Linux distribution with glibc >= {MIN_LINUX_GLIBC_DISPLAY}." + ))); + } + Ok(()) +} + +fn unsupported_semantic_platform(platform: &str) -> OrbitError { + OrbitError::InvalidInput(format!( + "semantic search unsupported on this platform ({platform}); supported companion platforms are macOS arm64 and Linux x86_64/aarch64 with glibc >= {MIN_LINUX_GLIBC_DISPLAY}" + )) +} + +fn parse_glibc_major_minor(version: &str) -> Option<(u32, u32)> { + let mut pieces = version.split('.'); + let major = pieces.next()?.parse::().ok()?; + let minor = pieces.next()?.parse::().ok()?; + Some((major, minor)) +} + +#[cfg(all(target_os = "linux", target_env = "gnu"))] +fn current_glibc_version() -> Option { + let version = { + // SAFETY: gnu_get_libc_version returns a process-static NUL-terminated + // string pointer, or null on failure; we only read it when non-null. + unsafe { libc::gnu_get_libc_version() } + }; + if version.is_null() { + return None; + } + let version = { + // SAFETY: non-null gnu_get_libc_version result points at a valid C string. + unsafe { CStr::from_ptr(version) } + }; + version.to_str().ok().map(str::to_string) +} + +#[cfg(not(all(target_os = "linux", target_env = "gnu")))] +fn current_glibc_version() -> Option { + None +} + pub fn locate_companion() -> Result { if let Ok(paths) = CompanionPaths::default_under_home() { let standard = paths.companion_path(); diff --git a/docs/RELEASE.md b/docs/RELEASE.md index 5377d180..e1f26c69 100644 --- a/docs/RELEASE.md +++ b/docs/RELEASE.md @@ -95,7 +95,16 @@ Each step names the exact file or command. Do them in order. - `smoke-install-macos` — installs the tagged macOS arm64 CLI, then runs `orbit semantic install --json` with isolated runtime state. - `smoke-install-ubuntu` — installs the tagged Linux x86_64 CLI, then runs - `orbit semantic install --json` with isolated runtime state. + `orbit semantic install --json` with isolated runtime state inside an + Ubuntu 24.04 container. + + The Linux CLI release binaries still build on Ubuntu 22.04 to keep the CLI + runtime floor low. The semantic companion is stricter: ONNX Runtime requires + glibc >= 2.38 at runtime, so Linux companion builds and semantic smoke tests + use Ubuntu 24.04 or newer. Released semantic companion assets currently + cover macOS arm64 and Linux x86_64/aarch64 with glibc >= 2.38. Intel macOS + receives a CLI asset, but semantic search is unsupported because the + companion has no x86_64-apple-darwin ONNX Runtime prebuilt. All five must be green before step 7.